bug 401240 - part 2/2 - reimplement PKCS#12 import/export without goto r=fkiefer

MozReview-Commit-ID: JUMmTPrEYND

--HG--
rename : security/manager/ssl/tests/unit/test_certDB_import_with_master_password.js => security/manager/ssl/tests/unit/test_certDB_export_pkcs12.js
rename : security/manager/ssl/tests/unit/test_certDB_import_with_master_password.js => security/manager/ssl/tests/unit/test_certDB_export_pkcs12_with_master_password.js
extra : rebase_source : 3bf01e722f496b4dba634ed5d0345ce1fe0ceea7
This commit is contained in:
David Keeler 2018-05-11 11:37:50 -07:00
parent 18117a994a
commit d8f748b996
9 changed files with 522 additions and 354 deletions

View File

@ -273,6 +273,7 @@ function backupCerts() {
fp.appendFilter(bundle.getString("file_browse_PKCS12_spec"),
"*.p12");
fp.appendFilters(nsIFilePicker.filterAll);
fp.defaultExtension = "p12";
fp.open(rv => {
if (rv == nsIFilePicker.returnOK || rv == nsIFilePicker.returnReplace) {
certdb.exportPKCS12File(fp.file, selected_certs.length, selected_certs);

View File

@ -356,6 +356,13 @@ MOZ_TYPE_SPECIFIC_UNIQUE_PTR_TEMPLATE(UniqueSGNDigestInfo,
MOZ_TYPE_SPECIFIC_UNIQUE_PTR_TEMPLATE(UniqueVFYContext,
VFYContext,
internal::VFY_DestroyContext_true)
MOZ_TYPE_SPECIFIC_UNIQUE_PTR_TEMPLATE(UniqueSEC_PKCS12DecoderContext,
SEC_PKCS12DecoderContext,
SEC_PKCS12DecoderFinish)
MOZ_TYPE_SPECIFIC_UNIQUE_PTR_TEMPLATE(UniqueSEC_PKCS12ExportContext,
SEC_PKCS12ExportContext,
SEC_PKCS12DestroyExportContext)
} // namespace mozilla
#endif // ScopedNSSTypes_h

View File

@ -2645,6 +2645,21 @@ setPassword(PK11SlotInfo* slot, nsIInterfaceRequestor* ctx)
return NS_OK;
}
// NSS will call this during PKCS12 export to potentially switch the endianness
// of the characters of `inBuf` to big (network) endian. Since we already did
// that in nsPKCS12Blob::stringToBigEndianBytes, we just perform a memcpy here.
extern "C" {
PRBool
pkcs12StringEndiannessConversion(PRBool, unsigned char* inBuf,
unsigned int inBufLen, unsigned char* outBuf,
unsigned int, unsigned int* outBufLen, PRBool)
{
*outBufLen = inBufLen;
memcpy(outBuf, inBuf, inBufLen);
return true;
}
}
namespace mozilla {
namespace psm {
@ -2680,7 +2695,7 @@ InitializeCipherSuite()
SEC_PKCS12EnableCipher(PKCS12_DES_56, 1);
SEC_PKCS12EnableCipher(PKCS12_DES_EDE3_168, 1);
SEC_PKCS12SetPreferredCipher(PKCS12_DES_EDE3_168, 1);
PORT_SetUCS2_ASCIIConversionFunction(pip_ucs2_ascii_conversion_fn);
PORT_SetUCS2_ASCIIConversionFunction(pkcs12StringEndiannessConversion);
// PSM enforces a minimum RSA key size of 1024 bits, which is overridable.
// NSS has its own minimum, which is not overridable (the default is 1023

View File

@ -1,20 +1,17 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
*
* This Source Code Form is subject to the terms of the Mozilla Public
/* -*- 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 NSS_HELPER_
#define NSS_HELPER_
#ifndef nsNSSHelper_h
#define nsNSSHelper_h
#include "nsIInterfaceRequestor.h"
#include "nsIInterfaceRequestorUtils.h"
#include "pk11func.h"
//
// Implementation of an nsIInterfaceRequestor for use
// as context for NSS calls
//
// Implementation of an nsIInterfaceRequestor for use as context for NSS calls.
class PipUIContext : public nsIInterfaceRequestor
{
public:
@ -27,30 +24,12 @@ protected:
virtual ~PipUIContext();
};
//
// Function to get the implementor for a certain set of NSS
// specific dialogs.
//
// Function to get the implementor for a certain set of NSS specific dialogs.
nsresult
getNSSDialogs(void **_result, REFNSIID aIID, const char *contract);
extern "C" {
// a "fake" unicode conversion function
PRBool
pip_ucs2_ascii_conversion_fn(PRBool toUnicode,
unsigned char *inBuf,
unsigned int inBufLen,
unsigned char *outBuf,
unsigned int maxOutBufLen,
unsigned int *outBufLen,
PRBool swapBytes);
}
//
// A function that sets the password on an unitialized slot.
//
nsresult
setPassword(PK11SlotInfo* slot, nsIInterfaceRequestor* ctx);
#endif
#endif // nsNSSHelper_h

View File

@ -26,143 +26,128 @@
using namespace mozilla;
extern LazyLogModule gPIPNSSLog;
#define PIP_PKCS12_TMPFILENAME NS_LITERAL_CSTRING(".pip_p12tmp")
#define PIP_PKCS12_BUFFER_SIZE 2048
#define PIP_PKCS12_USER_CANCELED 3
#define PIP_PKCS12_NOSMARTCARD_EXPORT 4
#define PIP_PKCS12_RESTORE_FAILED 5
#define PIP_PKCS12_BACKUP_FAILED 6
#define PIP_PKCS12_NSS_ERROR 7
// constructor
nsPKCS12Blob::nsPKCS12Blob()
: mCertArray(nullptr)
, mTmpFile(nullptr)
: mUIContext(new PipUIContext())
{
mUIContext = new PipUIContext();
}
// nsPKCS12Blob::ImportFromFile
//
// Given a file handle, read a PKCS#12 blob from that file, decode it, and
// import the results into the internal database.
nsresult
nsPKCS12Blob::ImportFromFile(nsIFile* file)
{
nsresult rv = NS_OK;
nsresult rv;
RetryReason wantRetry;
do {
rv = ImportFromFileHelper(file, im_standard_prompt, wantRetry);
rv = ImportFromFileHelper(file, ImportMode::StandardPrompt, wantRetry);
if (NS_SUCCEEDED(rv) && wantRetry == rr_auto_retry_empty_password_flavors) {
rv = ImportFromFileHelper(file, im_try_zero_length_secitem, wantRetry);
if (NS_SUCCEEDED(rv) && wantRetry == RetryReason::AutoRetryEmptyPassword) {
rv = ImportFromFileHelper(file, ImportMode::TryZeroLengthSecitem,
wantRetry);
}
} while (NS_SUCCEEDED(rv) && (wantRetry != rr_do_not_retry));
} while (NS_SUCCEEDED(rv) && (wantRetry != RetryReason::DoNotRetry));
return rv;
}
nsresult
nsPKCS12Blob::ImportFromFileHelper(nsIFile* file,
nsPKCS12Blob::ImportMode aImportMode,
nsPKCS12Blob::RetryReason& aWantRetry)
void
nsPKCS12Blob::handleImportError(PRErrorCode nssError, RetryReason& retryReason,
uint32_t passwordLengthInBytes)
{
nsresult rv = NS_OK;
SECStatus srv = SECSuccess;
SEC_PKCS12DecoderContext* dcx = nullptr;
SECItem unicodePw = { siBuffer, nullptr, 0 };
if (nssError == SEC_ERROR_BAD_PASSWORD) {
// If the password is 2 bytes, it only consists of the wide character null
// terminator. In this case we want to retry with a zero-length password.
if (passwordLengthInBytes == 2) {
retryReason = nsPKCS12Blob::RetryReason::AutoRetryEmptyPassword;
} else {
retryReason = RetryReason::BadPassword;
handleError(PIP_PKCS12_NSS_ERROR, nssError);
}
} else {
handleError(PIP_PKCS12_NSS_ERROR, nssError);
}
}
aWantRetry = rr_do_not_retry;
// Returns a failing nsresult if some XPCOM operation failed, and NS_OK
// otherwise. Returns by reference whether or not we want to retry the operation
// immediately.
nsresult
nsPKCS12Blob::ImportFromFileHelper(nsIFile* file, ImportMode aImportMode,
RetryReason& aWantRetry)
{
aWantRetry = RetryReason::DoNotRetry;
UniquePK11SlotInfo slot(PK11_GetInternalKeySlot());
if (!slot) {
srv = SECFailure;
goto finish;
return NS_ERROR_FAILURE;
}
if (aImportMode == im_try_zero_length_secitem) {
unicodePw.len = 0;
uint32_t passwordBufferLength;
UniquePtr<uint8_t[]> passwordBuffer;
if (aImportMode == ImportMode::TryZeroLengthSecitem) {
passwordBufferLength = 0;
passwordBuffer = nullptr;
} else {
// get file password (unicode)
rv = getPKCS12FilePassword(&unicodePw);
if (NS_FAILED(rv))
goto finish;
if (!unicodePw.data) {
handleError(PIP_PKCS12_USER_CANCELED);
nsresult rv = getPKCS12FilePassword(passwordBufferLength, passwordBuffer);
if (NS_FAILED(rv)) {
return rv;
}
if (!passwordBuffer) {
return NS_OK;
}
}
// initialize the decoder
dcx = SEC_PKCS12DecoderStart(&unicodePw,
slot.get(),
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr);
SECItem unicodePw = { siBuffer, passwordBuffer.get(), passwordBufferLength };
UniqueSEC_PKCS12DecoderContext dcx(
SEC_PKCS12DecoderStart(&unicodePw, slot.get(), nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr));
if (!dcx) {
srv = SECFailure;
goto finish;
return NS_ERROR_FAILURE;
}
// read input file and feed it to the decoder
rv = inputToDecoder(dcx, file);
PRErrorCode nssError;
nsresult rv = inputToDecoder(dcx, file, nssError);
if (NS_FAILED(rv)) {
if (NS_ERROR_ABORT == rv) {
// inputToDecoder indicated a NSS error
srv = SECFailure;
}
goto finish;
return rv;
}
if (nssError != 0) {
handleImportError(nssError, aWantRetry, unicodePw.len);
return NS_OK;
}
// verify the blob
srv = SEC_PKCS12DecoderVerify(dcx);
if (srv)
goto finish;
// validate bags
srv = SEC_PKCS12DecoderValidateBags(dcx, nickname_collision);
if (srv)
goto finish;
// import cert and key
srv = SEC_PKCS12DecoderImportBags(dcx);
if (srv)
goto finish;
// Later - check to see if this should become default email cert
finish:
// If srv != SECSuccess, NSS probably set a specific error code.
// We should use that error code instead of inventing a new one
// for every error possible.
SECStatus srv = SEC_PKCS12DecoderVerify(dcx.get());
if (srv != SECSuccess) {
if (SEC_ERROR_BAD_PASSWORD == PORT_GetError()) {
if (unicodePw.len == sizeof(char16_t)) {
// no password chars available,
// unicodeToItem allocated space for the trailing zero character only.
aWantRetry = rr_auto_retry_empty_password_flavors;
} else {
aWantRetry = rr_bad_password;
handleError(PIP_PKCS12_NSS_ERROR);
}
} else {
handleError(PIP_PKCS12_NSS_ERROR);
}
} else if (NS_FAILED(rv)) {
handleError(PIP_PKCS12_RESTORE_FAILED);
handleImportError(PR_GetError(), aWantRetry, unicodePw.len);
return NS_OK;
}
// validate bags
srv = SEC_PKCS12DecoderValidateBags(dcx.get(), nicknameCollision);
if (srv != SECSuccess) {
handleImportError(PR_GetError(), aWantRetry, unicodePw.len);
return NS_OK;
}
// import cert and key
srv = SEC_PKCS12DecoderImportBags(dcx.get());
if (srv != SECSuccess) {
handleImportError(PR_GetError(), aWantRetry, unicodePw.len);
}
// finish the decoder
if (dcx)
SEC_PKCS12DecoderFinish(dcx);
SECITEM_ZfreeItem(&unicodePw, false);
return NS_OK;
}
static bool
isExtractable(SECKEYPrivateKey* privKey)
isExtractable(UniqueSECKEYPrivateKey& privKey)
{
ScopedAutoSECItem value;
SECStatus rv =
PK11_ReadRawAttribute(PK11_TypePrivKey, privKey, CKA_EXTRACTABLE, &value);
SECStatus rv = PK11_ReadRawAttribute(
PK11_TypePrivKey, privKey.get(), CKA_EXTRACTABLE, &value);
if (rv != SECSuccess) {
return false;
}
@ -174,266 +159,216 @@ isExtractable(SECKEYPrivateKey* privKey)
return isExtractable;
}
// nsPKCS12Blob::ExportToFile
//
// Having already loaded the certs, form them into a blob (loading the keys
// also), encode the blob, and stuff it into the file.
nsresult
nsPKCS12Blob::ExportToFile(nsIFile* file, nsIX509Cert** certs, int numCerts)
{
nsresult rv;
SECStatus srv = SECSuccess;
SEC_PKCS12ExportContext* ecx = nullptr;
SEC_PKCS12SafeInfo *certSafe = nullptr, *keySafe = nullptr;
SECItem unicodePw;
nsAutoString filePath;
int i;
nsCOMPtr<nsIFile> localFileRef;
// init slot
bool InformedUserNoSmartcardBackup = false;
int numCertsExported = 0;
bool informedUserNoSmartcardBackup = false;
// get file password (unicode)
unicodePw.data = nullptr;
rv = newPKCS12FilePassword(&unicodePw);
if (NS_FAILED(rv))
goto finish;
if (!unicodePw.data) {
handleError(PIP_PKCS12_USER_CANCELED);
uint32_t passwordBufferLength;
UniquePtr<uint8_t[]> passwordBuffer;
nsresult rv = newPKCS12FilePassword(passwordBufferLength, passwordBuffer);
if (NS_FAILED(rv)) {
return rv;
}
if (!passwordBuffer) {
return NS_OK;
}
// what about slotToUse in psm 1.x ???
// create export context
ecx =
SEC_PKCS12CreateExportContext(nullptr, nullptr, nullptr /*slot*/, nullptr);
UniqueSEC_PKCS12ExportContext ecx(
SEC_PKCS12CreateExportContext(nullptr, nullptr, nullptr, nullptr));
if (!ecx) {
srv = SECFailure;
goto finish;
handleError(PIP_PKCS12_BACKUP_FAILED, PR_GetError());
return NS_ERROR_FAILURE;
}
// add password integrity
srv = SEC_PKCS12AddPasswordIntegrity(ecx, &unicodePw, SEC_OID_SHA1);
if (srv)
goto finish;
for (i = 0; i < numCerts; i++) {
nsNSSCertificate* cert = (nsNSSCertificate*)certs[i];
// get it as a CERTCertificate XXX
UniqueCERTCertificate nssCert(cert->GetCert());
SECItem unicodePw = { siBuffer, passwordBuffer.get(), passwordBufferLength };
SECStatus srv = SEC_PKCS12AddPasswordIntegrity(ecx.get(), &unicodePw,
SEC_OID_SHA1);
if (srv != SECSuccess) {
handleError(PIP_PKCS12_BACKUP_FAILED, PR_GetError());
return NS_ERROR_FAILURE;
}
for (int i = 0; i < numCerts; i++) {
UniqueCERTCertificate nssCert(certs[i]->GetCert());
if (!nssCert) {
rv = NS_ERROR_FAILURE;
goto finish;
handleError(PIP_PKCS12_BACKUP_FAILED, PR_GetError());
return NS_ERROR_FAILURE;
}
// We can only successfully export certs that are on
// internal token. Most, if not all, smart card vendors
// won't let you extract the private key (in any way
// shape or form) from the card. So let's punt if
// We can probably only successfully export certs that are on the internal
// token. Most, if not all, smart card vendors won't let you extract the
// private key (in any way shape or form) from the card. So let's punt if
// the cert is not in the internal db.
if (nssCert->slot && !PK11_IsInternal(nssCert->slot)) {
// we aren't the internal token, see if the key is extractable.
SECKEYPrivateKey* privKey =
PK11_FindKeyByDERCert(nssCert->slot, nssCert.get(), this);
if (privKey) {
bool privKeyIsExtractable = isExtractable(privKey);
SECKEY_DestroyPrivateKey(privKey);
if (!privKeyIsExtractable) {
if (!InformedUserNoSmartcardBackup) {
InformedUserNoSmartcardBackup = true;
handleError(PIP_PKCS12_NOSMARTCARD_EXPORT);
}
continue;
// We aren't the internal token, see if the key is extractable.
UniqueSECKEYPrivateKey privKey(
PK11_FindKeyByDERCert(nssCert->slot, nssCert.get(), mUIContext));
if (privKey && !isExtractable(privKey)) {
if (!informedUserNoSmartcardBackup) {
informedUserNoSmartcardBackup = true;
handleError(PIP_PKCS12_NOSMARTCARD_EXPORT, PR_GetError());
}
continue;
}
}
// XXX this is why, to verify the slot is the same
// PK11_FindObjectForCert(nssCert, nullptr, slot);
// create the cert and key safes
keySafe = SEC_PKCS12CreateUnencryptedSafe(ecx);
// certSafe and keySafe are owned by ecx.
SEC_PKCS12SafeInfo* certSafe;
SEC_PKCS12SafeInfo* keySafe = SEC_PKCS12CreateUnencryptedSafe(ecx.get());
if (!SEC_PKCS12IsEncryptionAllowed() || PK11_IsFIPS()) {
certSafe = keySafe;
} else {
certSafe = SEC_PKCS12CreatePasswordPrivSafe(
ecx, &unicodePw, SEC_OID_PKCS12_V2_PBE_WITH_SHA1_AND_40_BIT_RC2_CBC);
ecx.get(), &unicodePw,
SEC_OID_PKCS12_V2_PBE_WITH_SHA1_AND_40_BIT_RC2_CBC);
}
if (!certSafe || !keySafe) {
rv = NS_ERROR_FAILURE;
goto finish;
handleError(PIP_PKCS12_BACKUP_FAILED, PR_GetError());
return NS_ERROR_FAILURE;
}
// add the cert and key to the blob
srv = SEC_PKCS12AddCertAndKey(
ecx,
ecx.get(),
certSafe,
nullptr,
nssCert.get(),
CERT_GetDefaultCertDB(), // XXX
CERT_GetDefaultCertDB(),
keySafe,
nullptr,
true,
&unicodePw,
SEC_OID_PKCS12_V2_PBE_WITH_SHA1_AND_3KEY_TRIPLE_DES_CBC);
if (srv)
goto finish;
// cert was dup'ed, so release it
++numCertsExported;
if (srv != SECSuccess) {
handleError(PIP_PKCS12_BACKUP_FAILED, PR_GetError());
return NS_ERROR_FAILURE;
}
}
if (!numCertsExported)
goto finish;
// prepare the instance to write to an export file
this->mTmpFile = nullptr;
file->GetPath(filePath);
// Use the nsCOMPtr var localFileRef so that
// the reference to the nsIFile we create gets released as soon as
// we're out of scope, ie when this function exits.
if (filePath.RFind(".p12", true, -1, 4) < 0) {
// We're going to add the .p12 extension to the file name just like
// Communicator used to. We create a new nsIFile and initialize
// it with the new patch.
filePath.AppendLiteral(".p12");
localFileRef = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
if (NS_FAILED(rv))
goto finish;
localFileRef->InitWithPath(filePath);
file = localFileRef;
}
UniquePRFileDesc prFile;
PRFileDesc* rawPRFile;
rv = file->OpenNSPRFileDesc(
PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE, 0664, &mTmpFile);
if (NS_FAILED(rv) || !this->mTmpFile)
goto finish;
PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE, 0664, &rawPRFile);
if (NS_FAILED(rv) || !rawPRFile) {
handleError(PIP_PKCS12_BACKUP_FAILED, PR_GetError());
return NS_ERROR_FAILURE;
}
prFile.reset(rawPRFile);
// encode and write
srv = SEC_PKCS12Encode(ecx, write_export_file, this);
if (srv)
goto finish;
finish:
if (NS_FAILED(rv) || srv != SECSuccess) {
handleError(PIP_PKCS12_BACKUP_FAILED);
srv = SEC_PKCS12Encode(ecx.get(), writeExportFile, prFile.get());
if (srv != SECSuccess) {
handleError(PIP_PKCS12_BACKUP_FAILED, PR_GetError());
return NS_ERROR_FAILURE;
}
if (ecx)
SEC_PKCS12DestroyExportContext(ecx);
if (this->mTmpFile) {
PR_Close(this->mTmpFile);
this->mTmpFile = nullptr;
}
SECITEM_ZfreeItem(&unicodePw, false);
return rv;
return NS_OK;
}
///////////////////////////////////////////////////////////////////////
//
// private members
//
///////////////////////////////////////////////////////////////////////
// unicodeToItem
//
// For the NSS PKCS#12 library, must convert PRUnichars (shorts) to
// a buffer of octets. Must handle byte order correctly.
nsresult
nsPKCS12Blob::unicodeToItem(const nsString& uni, SECItem* item)
// For the NSS PKCS#12 library, must convert PRUnichars (shorts) to a buffer of
// octets. Must handle byte order correctly.
UniquePtr<uint8_t[]>
nsPKCS12Blob::stringToBigEndianBytes(const nsString& uni, uint32_t& bytesLength)
{
uint32_t len = uni.Length() + 1; // +1 for the null terminator.
if (!SECITEM_AllocItem(nullptr, item, sizeof(char16_t) * len)) {
return NS_ERROR_OUT_OF_MEMORY;
}
uint32_t wideLength = uni.Length() + 1; // +1 for the null terminator.
bytesLength = wideLength * 2;
auto buffer = MakeUnique<uint8_t[]>(bytesLength);
// We have to use a cast here because on Windows, uni.get() returns
// char16ptr_t instead of char16_t*.
mozilla::NativeEndian::copyAndSwapToBigEndian(
item->data, static_cast<const char16_t*>(uni.get()), len);
buffer.get(), static_cast<const char16_t*>(uni.get()), wideLength);
return NS_OK;
return buffer;
}
// newPKCS12FilePassword
//
// Launch a dialog requesting the user for a new PKCS#12 file passowrd.
// Handle user canceled by returning null password (caller must catch).
nsresult
nsPKCS12Blob::newPKCS12FilePassword(SECItem* unicodePw)
nsPKCS12Blob::newPKCS12FilePassword(uint32_t& passwordBufferLength,
UniquePtr<uint8_t[]>& passwordBuffer)
{
nsresult rv = NS_OK;
nsAutoString password;
nsCOMPtr<nsICertificateDialogs> certDialogs;
rv = ::getNSSDialogs(getter_AddRefs(certDialogs),
NS_GET_IID(nsICertificateDialogs),
NS_CERTIFICATEDIALOGS_CONTRACTID);
if (NS_FAILED(rv))
nsresult rv = ::getNSSDialogs(getter_AddRefs(certDialogs),
NS_GET_IID(nsICertificateDialogs),
NS_CERTIFICATEDIALOGS_CONTRACTID);
if (NS_FAILED(rv)) {
return rv;
bool pressedOK;
}
bool pressedOK = false;
rv = certDialogs->SetPKCS12FilePassword(mUIContext, password, &pressedOK);
if (NS_FAILED(rv) || !pressedOK)
if (NS_FAILED(rv)) {
return rv;
return unicodeToItem(password, unicodePw);
}
if (!pressedOK) {
return NS_OK;
}
passwordBuffer = Move(stringToBigEndianBytes(password, passwordBufferLength));
return NS_OK;
}
// getPKCS12FilePassword
//
// Launch a dialog requesting the user for the password to a PKCS#12 file.
// Handle user canceled by returning null password (caller must catch).
nsresult
nsPKCS12Blob::getPKCS12FilePassword(SECItem* unicodePw)
nsPKCS12Blob::getPKCS12FilePassword(uint32_t& passwordBufferLength,
UniquePtr<uint8_t[]>& passwordBuffer)
{
nsresult rv = NS_OK;
nsAutoString password;
nsCOMPtr<nsICertificateDialogs> certDialogs;
rv = ::getNSSDialogs(getter_AddRefs(certDialogs),
NS_GET_IID(nsICertificateDialogs),
NS_CERTIFICATEDIALOGS_CONTRACTID);
if (NS_FAILED(rv))
nsresult rv = ::getNSSDialogs(getter_AddRefs(certDialogs),
NS_GET_IID(nsICertificateDialogs),
NS_CERTIFICATEDIALOGS_CONTRACTID);
if (NS_FAILED(rv)) {
return rv;
bool pressedOK;
}
nsAutoString password;
bool pressedOK = false;
rv = certDialogs->GetPKCS12FilePassword(mUIContext, password, &pressedOK);
if (NS_FAILED(rv) || !pressedOK)
if (NS_FAILED(rv)) {
return rv;
return unicodeToItem(password, unicodePw);
}
if (!pressedOK) {
return NS_OK;
}
passwordBuffer = Move(stringToBigEndianBytes(password, passwordBufferLength));
return NS_OK;
}
// inputToDecoder
//
// Given a decoder, read bytes from file and input them to the decoder.
nsresult
nsPKCS12Blob::inputToDecoder(SEC_PKCS12DecoderContext* dcx, nsIFile* file)
nsPKCS12Blob::inputToDecoder(UniqueSEC_PKCS12DecoderContext& dcx, nsIFile* file,
PRErrorCode& nssError)
{
nsresult rv;
SECStatus srv;
uint32_t amount;
char buf[PIP_PKCS12_BUFFER_SIZE];
nssError = 0;
nsCOMPtr<nsIInputStream> fileStream;
rv = NS_NewLocalFileInputStream(getter_AddRefs(fileStream), file);
nsresult rv = NS_NewLocalFileInputStream(getter_AddRefs(fileStream), file);
if (NS_FAILED(rv)) {
return rv;
}
char buf[PIP_PKCS12_BUFFER_SIZE];
uint32_t amount;
while (true) {
rv = fileStream->Read(buf, PIP_PKCS12_BUFFER_SIZE, &amount);
if (NS_FAILED(rv)) {
return rv;
}
// feed the file data into the decoder
srv = SEC_PKCS12DecoderUpdate(dcx, (unsigned char*)buf, amount);
if (srv) {
// don't allow the close call to overwrite our precious error code
int pr_err = PORT_GetError();
PORT_SetError(pr_err);
return NS_ERROR_ABORT;
SECStatus srv = SEC_PKCS12DecoderUpdate(
dcx.get(), (unsigned char*)buf, amount);
if (srv != SECSuccess) {
nssError = PR_GetError();
return NS_OK;
}
if (amount < PIP_PKCS12_BUFFER_SIZE)
if (amount < PIP_PKCS12_BUFFER_SIZE) {
break;
}
}
return NS_OK;
}
// nickname_collision
// what to do when the nickname collides with one already in the db.
// TODO: not handled, throw a dialog allowing the nick to be changed?
// What to do when the nickname collides with one already in the db.
SECItem*
nsPKCS12Blob::nickname_collision(SECItem* oldNick, PRBool* cancel, void* wincx)
nsPKCS12Blob::nicknameCollision(SECItem* oldNick, PRBool* cancel, void* wincx)
{
*cancel = false;
int count = 1;
@ -479,60 +414,40 @@ nsPKCS12Blob::nickname_collision(SECItem* oldNick, PRBool* cancel, void* wincx)
}
count++;
}
SECItem* newNick = new SECItem;
if (!newNick)
UniqueSECItem newNick(SECITEM_AllocItem(nullptr, nullptr,
nickname.Length() + 1));
if (!newNick) {
return nullptr;
}
memcpy(newNick->data, nickname.get(), nickname.Length());
newNick->data[nickname.Length()] = 0;
newNick->type = siAsciiString;
newNick->data = (unsigned char*)strdup(nickname.get());
newNick->len = strlen((char*)newNick->data);
return newNick;
return newNick.release();
}
// write_export_file
// write bytes to the exported PKCS#12 file
void
nsPKCS12Blob::write_export_file(void* arg, const char* buf, unsigned long len)
nsPKCS12Blob::writeExportFile(void* arg, const char* buf, unsigned long len)
{
nsPKCS12Blob* cx = (nsPKCS12Blob*)arg;
PR_Write(cx->mTmpFile, buf, len);
}
// pip_ucs2_ascii_conversion_fn
// required to be set by NSS (to do PKCS#12), but since we've already got
// unicode make this a no-op.
PRBool
pip_ucs2_ascii_conversion_fn(PRBool toUnicode,
unsigned char* inBuf,
unsigned int inBufLen,
unsigned char* outBuf,
unsigned int maxOutBufLen,
unsigned int* outBufLen,
PRBool swapBytes)
{
// do a no-op, since I've already got unicode. Hah!
*outBufLen = inBufLen;
memcpy(outBuf, inBuf, inBufLen);
return true;
PRFileDesc* file = static_cast<PRFileDesc*>(arg);
MOZ_RELEASE_ASSERT(file);
PR_Write(file, buf, len);
}
void
nsPKCS12Blob::handleError(int myerr)
nsPKCS12Blob::handleError(int myerr, PRErrorCode prerr)
{
MOZ_ASSERT(NS_IsMainThread());
if (!NS_IsMainThread()) {
return;
}
int prerr = PORT_GetError();
MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("PKCS12: NSS/NSPR error(%d)", prerr));
MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("PKCS12: I called(%d)", myerr));
const char* msgID = nullptr;
switch (myerr) {
case PIP_PKCS12_USER_CANCELED:
return; /* Just ignore it for now */
case PIP_PKCS12_NOSMARTCARD_EXPORT:
msgID = "PKCS12InfoNoSmartcardBackup";
break;
@ -544,15 +459,11 @@ nsPKCS12Blob::handleError(int myerr)
break;
case PIP_PKCS12_NSS_ERROR:
switch (prerr) {
// The following errors have the potential to be "handled", by asking
// the user (via a dialog) whether s/he wishes to continue
case 0:
break;
case SEC_ERROR_PKCS12_CERT_COLLISION:
/* pop a dialog saying the cert is already in the database */
/* ask to keep going? what happens if one collision but others ok? */
// The following errors cannot be "handled", notify the user (via an
// alert) that the operation failed.
msgID = "PKCS12DupData";
break;
case SEC_ERROR_BAD_PASSWORD:
msgID = "PK11BadPassword";
break;
@ -570,8 +481,9 @@ nsPKCS12Blob::handleError(int myerr)
break;
}
if (!msgID)
if (!msgID) {
msgID = "PKCS12UnknownErr";
}
nsCOMPtr<nsIWindowWatcher> wwatch(do_GetService(NS_WINDOWWATCHER_CONTRACTID));
if (!wwatch) {

View File

@ -1,3 +1,5 @@
/* -*- 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/. */
@ -14,16 +16,12 @@
class nsIFile;
class nsIX509Cert;
//
// nsPKCS12Blob
//
// Class for importing/exporting PKCS#12 blobs
//
class nsPKCS12Blob
{
public:
nsPKCS12Blob();
virtual ~nsPKCS12Blob() {}
~nsPKCS12Blob() {}
// PKCS#12 Import
nsresult ImportFromFile(nsIFile* file);
@ -32,50 +30,51 @@ public:
nsresult ExportToFile(nsIFile* file, nsIX509Cert** certs, int numCerts);
private:
nsCOMPtr<nsIMutableArray> mCertArray;
nsCOMPtr<nsIInterfaceRequestor> mUIContext;
// local helper functions
nsresult getPKCS12FilePassword(SECItem*);
nsresult newPKCS12FilePassword(SECItem*);
nsresult inputToDecoder(SEC_PKCS12DecoderContext*, nsIFile*);
nsresult unicodeToItem(const nsString& uni, SECItem* item);
void handleError(int myerr = 0);
nsresult getPKCS12FilePassword(uint32_t& passwordBufferLength,
UniquePtr<uint8_t[]>& passwordBuffer);
nsresult newPKCS12FilePassword(uint32_t& passwordBufferLength,
UniquePtr<uint8_t[]>& passwordBuffer);
nsresult inputToDecoder(UniqueSEC_PKCS12DecoderContext& dcx, nsIFile* file,
PRErrorCode& nssError);
UniquePtr<uint8_t[]> stringToBigEndianBytes(const nsString& uni,
uint32_t& bytesLength);
void handleError(int myerr, PRErrorCode prerr);
// RetryReason and ImportMode are used when importing a PKCS12 file.
// There are two reasons that cause us to retry:
// - When the password entered by the user is incorrect.
// The user will be prompted to try again.
// - When the user entered a zero length password.
// An empty password should be represented as an empty
// string (a SECItem that contains a single terminating
// null UTF16 character), but some applications use a
// zero length SECItem.
// We try both variations, zero length item and empty string,
// without giving a user prompt when trying the different empty password
// flavors.
// An empty password should be represented as an empty string (a SECItem
// that contains a single terminating null UTF16 character), but some
// applications use a zero length SECItem. We try both variations, zero
// length item and empty string, without giving a user prompt when trying
// the different empty password flavors.
enum class RetryReason
{
DoNotRetry,
BadPassword,
AutoRetryEmptyPassword,
};
enum class ImportMode
{
StandardPrompt,
TryZeroLengthSecitem
};
enum RetryReason
{
rr_do_not_retry,
rr_bad_password,
rr_auto_retry_empty_password_flavors
};
enum ImportMode
{
im_standard_prompt,
im_try_zero_length_secitem
};
void handleImportError(PRErrorCode nssError, RetryReason& retryReason,
uint32_t passwordLengthInBytes);
nsresult ImportFromFileHelper(nsIFile* file,
ImportMode aImportMode,
RetryReason& aWantRetry);
// NSPR file I/O for export file
PRFileDesc* mTmpFile;
static SECItem* nickname_collision(SECItem*, PRBool*, void*);
static void write_export_file(void* arg, const char* buf, unsigned long len);
static SECItem* nicknameCollision(SECItem* oldNick, PRBool* cancel,
void* wincx);
static void writeExportFile(void* arg, const char* buf, unsigned long len);
};
#endif // nsPKCS12Blob_h

View File

@ -0,0 +1,115 @@
// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
// Any copyright is dedicated to the Public Domain.
// http://creativecommons.org/publicdomain/zero/1.0/
"use strict";
// Tests exporting a certificate and key as a PKCS#12 blob and importing it
// again with a new password set.
do_get_profile();
const gCertDB = Cc["@mozilla.org/security/x509certdb;1"]
.getService(Ci.nsIX509CertDB);
const PKCS12_FILE = "test_certDB_import/cert_from_windows.pfx";
const CERT_COMMON_NAME = "test_cert_from_windows";
const TEST_CERT_PASSWORD = "黒い";
const TEST_OUTPUT_PASSWORD = "other password";
let gPasswordToUse = TEST_CERT_PASSWORD;
// Mock implementation of nsICertificateDialogs.
const gCertificateDialogs = {
confirmDownloadCACert: () => {
// We don't test anything that calls this method.
ok(false, "confirmDownloadCACert() should not have been called");
},
setPKCS12FilePassword: (ctx, password) => {
password.value = gPasswordToUse;
return true;
},
getPKCS12FilePassword: (ctx, password) => {
password.value = gPasswordToUse;
return true;
},
viewCert: (ctx, cert) => {
// This shouldn't be called for import methods.
ok(false, "viewCert() should not have been called");
},
QueryInterface: ChromeUtils.generateQI([Ci.nsICertificateDialogs])
};
var gPrompt = {
clickOk: true,
QueryInterface: ChromeUtils.generateQI([Ci.nsIPrompt]),
// This intentionally does not use arrow function syntax to avoid an issue
// where in the context of the arrow function, |this != gPrompt| due to
// how objects get wrapped when going across xpcom boundaries.
alert(title, text) {
ok(false, "Not expecting alert to be called.");
},
promptPassword(dialogTitle, text, password, checkMsg, checkValue) {
ok(false, "Not expecting a password prompt.");
return false;
},
};
const gPromptFactory = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIPromptFactory]),
getPrompt: (aWindow, aIID) => gPrompt,
};
function findCertByCommonName(commonName) {
let certEnumerator = gCertDB.getCerts().getEnumerator();
while (certEnumerator.hasMoreElements()) {
let cert = certEnumerator.getNext().QueryInterface(Ci.nsIX509Cert);
if (cert.commonName == commonName) {
return cert;
}
}
return null;
}
function run_test() {
let certificateDialogsCID =
MockRegistrar.register("@mozilla.org/nsCertificateDialogs;1",
gCertificateDialogs);
let promptFactoryCID =
MockRegistrar.register("@mozilla.org/prompter;1", gPromptFactory);
registerCleanupFunction(() => {
MockRegistrar.unregister(certificateDialogsCID);
MockRegistrar.unregister(promptFactoryCID);
});
// Import the certificate and key so we have something to export.
let cert = findCertByCommonName(CERT_COMMON_NAME);
equal(cert, null, "cert should not be found before import");
let certFile = do_get_file(PKCS12_FILE);
ok(certFile, `${PKCS12_FILE} should exist`);
gPasswordToUse = TEST_CERT_PASSWORD;
gCertDB.importPKCS12File(certFile);
cert = findCertByCommonName(CERT_COMMON_NAME);
notEqual(cert, null, "cert should be found now");
// Export the certificate and key.
let output = do_get_tempdir();
output.append("output.p12");
ok(!output.exists(), "output shouldn't exist before exporting PKCS12 file");
gPasswordToUse = TEST_OUTPUT_PASSWORD;
gCertDB.exportPKCS12File(output, 1, [cert]);
ok(output.exists(), "output should exist after exporting PKCS12 file");
// We should be able to import the exported blob again using the new password.
gCertDB.importPKCS12File(output);
output.remove(false /* not a directory; recursive doesn't apply */);
// Ideally there would be some way to confirm that this actually did anything.
// Unfortunately, since deleting a certificate currently doesn't actually do
// anything until the platform is restarted, we can't confirm that we
// successfully re-imported the certificate.
}

View File

@ -0,0 +1,138 @@
// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
// Any copyright is dedicated to the Public Domain.
// http://creativecommons.org/publicdomain/zero/1.0/
"use strict";
// Tests exporting a certificate and key as a PKCS#12 blob if the user has a
// master password set.
do_get_profile();
const gCertDB = Cc["@mozilla.org/security/x509certdb;1"]
.getService(Ci.nsIX509CertDB);
const PKCS12_FILE = "test_certDB_import/cert_from_windows.pfx";
const CERT_COMMON_NAME = "test_cert_from_windows";
const TEST_CERT_PASSWORD = "黒い";
// Mock implementation of nsICertificateDialogs.
const gCertificateDialogs = {
confirmDownloadCACert: () => {
// We don't test anything that calls this method.
ok(false, "confirmDownloadCACert() should not have been called");
},
setPKCS12FilePassword: (ctx, password) => {
password.value = TEST_CERT_PASSWORD;
return true;
},
getPKCS12FilePassword: (ctx, password) => {
password.value = TEST_CERT_PASSWORD;
return true;
},
viewCert: (ctx, cert) => {
// This shouldn't be called for import methods.
ok(false, "viewCert() should not have been called");
},
QueryInterface: ChromeUtils.generateQI([Ci.nsICertificateDialogs])
};
var gPrompt = {
password: "password",
clickOk: true,
expectingAlert: false,
expectedAlertRegexp: null,
QueryInterface: ChromeUtils.generateQI([Ci.nsIPrompt]),
// This intentionally does not use arrow function syntax to avoid an issue
// where in the context of the arrow function, |this != gPrompt| due to
// how objects get wrapped when going across xpcom boundaries.
alert(title, text) {
info(`alert('${text}')`);
ok(this.expectingAlert,
"alert() should only be called if we're expecting it");
ok(this.expectedAlertRegexp.test(text),
"alert text should match expected message");
},
promptPassword(dialogTitle, text, password, checkMsg, checkValue) {
equal(text,
"Please enter your master password.",
"password prompt text should be as expected");
equal(checkMsg, null, "checkMsg should be null");
password.value = this.password;
return this.clickOk;
},
};
const gPromptFactory = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIPromptFactory]),
getPrompt: (aWindow, aIID) => gPrompt,
};
function findCertByCommonName(commonName) {
let certEnumerator = gCertDB.getCerts().getEnumerator();
while (certEnumerator.hasMoreElements()) {
let cert = certEnumerator.getNext().QueryInterface(Ci.nsIX509Cert);
if (cert.commonName == commonName) {
return cert;
}
}
return null;
}
function run_test() {
let certificateDialogsCID =
MockRegistrar.register("@mozilla.org/nsCertificateDialogs;1",
gCertificateDialogs);
let promptFactoryCID =
MockRegistrar.register("@mozilla.org/prompter;1", gPromptFactory);
registerCleanupFunction(() => {
MockRegistrar.unregister(certificateDialogsCID);
MockRegistrar.unregister(promptFactoryCID);
});
// Set a master password.
let tokenDB = Cc["@mozilla.org/security/pk11tokendb;1"]
.getService(Ci.nsIPK11TokenDB);
let token = tokenDB.getInternalKeyToken();
token.initPassword("password");
token.logoutSimple();
// Import the certificate and key so we have something to export.
let cert = findCertByCommonName(CERT_COMMON_NAME);
equal(cert, null, "cert should not be found before import");
let certFile = do_get_file(PKCS12_FILE);
ok(certFile, `${PKCS12_FILE} should exist`);
gCertDB.importPKCS12File(certFile);
cert = findCertByCommonName(CERT_COMMON_NAME);
notEqual(cert, null, "cert should be found now");
// Log out so we're prompted for the password.
token.logoutSimple();
// Export the certificate and key (and don't cancel the password request
// dialog).
let output = do_get_tempdir();
output.append("output.p12");
ok(!output.exists(), "output shouldn't exist before exporting PKCS12 file");
gCertDB.exportPKCS12File(output, 1, [cert]);
ok(output.exists(), "output should exist after exporting PKCS12 file");
output.remove(false /* not a directory; recursive doesn't apply */);
// Log out again so we're prompted for the password.
token.logoutSimple();
// Attempt to export the certificate and key, but this time cancel the
// password request dialog. The export operation should also be canceled.
gPrompt.clickOk = false;
let output2 = do_get_tempdir();
output2.append("output2.p12");
ok(!output2.exists(), "output2 shouldn't exist before exporting PKCS12 file");
gPrompt.expectingAlert = true;
gPrompt.expectedAlertRegexp = /Failed to create the PKCS #12 backup file for unknown reasons\./;
throws(() => gCertDB.exportPKCS12File(output, 1, [cert]), /NS_ERROR_FAILURE/);
ok(!output2.exists(), "output2 shouldn't exist after failing to export");
}

View File

@ -63,6 +63,8 @@ run-sequentially = hardcoded ports
[test_cert_signatures.js]
[test_cert_trust.js]
[test_cert_version.js]
[test_certDB_export_pkcs12.js]
[test_certDB_export_pkcs12_with_master_password.js]
[test_certDB_import.js]
[test_certDB_import_pkcs12.js]
[test_certDB_import_with_master_password.js]