358 lines
13 KiB
C++
358 lines
13 KiB
C++
/* 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/. */
|
|
|
|
#include "nsPKCS12Blob.h"
|
|
|
|
#include "mozilla/Assertions.h"
|
|
#include "mozilla/Casting.h"
|
|
#include "mozilla/Logging.h"
|
|
#include "mozilla/Preferences.h"
|
|
#include "mozilla/StaticPrefs_security.h"
|
|
#include "mozilla/Unused.h"
|
|
#include "mozpkix/pkixtypes.h"
|
|
#include "nsIFile.h"
|
|
#include "nsIInputStream.h"
|
|
#include "nsIX509CertDB.h"
|
|
#include "nsNetUtil.h"
|
|
#include "nsNSSCertHelper.h"
|
|
#include "nsNSSCertificate.h"
|
|
#include "nsNSSHelper.h"
|
|
#include "nsReadableUtils.h"
|
|
#include "nsTArray.h"
|
|
#include "nsThreadUtils.h"
|
|
#include "p12plcy.h"
|
|
#include "ScopedNSSTypes.h"
|
|
#include "secerr.h"
|
|
|
|
using namespace mozilla;
|
|
extern LazyLogModule gPIPNSSLog;
|
|
|
|
#define PIP_PKCS12_BUFFER_SIZE 2048
|
|
#define PIP_PKCS12_NOSMARTCARD_EXPORT 4
|
|
#define PIP_PKCS12_RESTORE_FAILED 5
|
|
#define PIP_PKCS12_BACKUP_FAILED 6
|
|
#define PIP_PKCS12_NSS_ERROR 7
|
|
|
|
nsPKCS12Blob::nsPKCS12Blob() : mUIContext(new PipUIContext()) {}
|
|
|
|
// 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* aFile,
|
|
const nsAString& aPassword,
|
|
uint32_t& aError) {
|
|
uint32_t passwordBufferLength;
|
|
UniquePtr<uint8_t[]> passwordBuffer;
|
|
|
|
UniquePK11SlotInfo slot(PK11_GetInternalKeySlot());
|
|
if (!slot) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
passwordBuffer = stringToBigEndianBytes(aPassword, passwordBufferLength);
|
|
|
|
// initialize the decoder
|
|
SECItem unicodePw = {siBuffer, passwordBuffer.get(), passwordBufferLength};
|
|
UniqueSEC_PKCS12DecoderContext dcx(
|
|
SEC_PKCS12DecoderStart(&unicodePw, slot.get(), nullptr, nullptr, nullptr,
|
|
nullptr, nullptr, nullptr));
|
|
if (!dcx) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
// read input aFile and feed it to the decoder
|
|
PRErrorCode nssError;
|
|
nsresult rv = inputToDecoder(dcx, aFile, nssError);
|
|
if (NS_FAILED(rv)) {
|
|
return rv;
|
|
}
|
|
if (nssError != 0) {
|
|
aError = handlePRErrorCode(nssError);
|
|
return NS_OK;
|
|
}
|
|
// verify the blob
|
|
SECStatus srv = SEC_PKCS12DecoderVerify(dcx.get());
|
|
if (srv != SECSuccess) {
|
|
aError = handlePRErrorCode(PR_GetError());
|
|
return NS_OK;
|
|
}
|
|
// validate bags
|
|
srv = SEC_PKCS12DecoderValidateBags(dcx.get(), nicknameCollision);
|
|
if (srv != SECSuccess) {
|
|
aError = handlePRErrorCode(PR_GetError());
|
|
return NS_OK;
|
|
}
|
|
// import cert and key
|
|
srv = SEC_PKCS12DecoderImportBags(dcx.get());
|
|
if (srv != SECSuccess) {
|
|
aError = handlePRErrorCode(PR_GetError());
|
|
return NS_OK;
|
|
}
|
|
aError = nsIX509CertDB::Success;
|
|
return NS_OK;
|
|
}
|
|
|
|
static bool isExtractable(UniqueSECKEYPrivateKey& privKey) {
|
|
ScopedAutoSECItem value;
|
|
SECStatus rv = PK11_ReadRawAttribute(PK11_TypePrivKey, privKey.get(),
|
|
CKA_EXTRACTABLE, &value);
|
|
if (rv != SECSuccess) {
|
|
return false;
|
|
}
|
|
|
|
bool isExtractable = false;
|
|
if ((value.len == 1) && value.data) {
|
|
isExtractable = !!(*(CK_BBOOL*)value.data);
|
|
}
|
|
return isExtractable;
|
|
}
|
|
|
|
// 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* aFile,
|
|
const nsTArray<RefPtr<nsIX509Cert>>& aCerts,
|
|
const nsAString& aPassword,
|
|
uint32_t& aError) {
|
|
nsCString passwordUtf8 = NS_ConvertUTF16toUTF8(aPassword);
|
|
uint32_t passwordBufferLength = passwordUtf8.Length();
|
|
aError = nsIX509CertDB::Success;
|
|
// The conversion to UCS2 is executed by sec_pkcs12_encode_password when
|
|
// necessary (for some older PKCS12 algorithms). The NSS 3.31 and newer
|
|
// expects password to be in the utf8 encoding to support modern encoders.
|
|
UniquePtr<unsigned char[]> passwordBuffer(
|
|
reinterpret_cast<unsigned char*>(ToNewCString(passwordUtf8)));
|
|
if (!passwordBuffer.get()) {
|
|
return NS_OK;
|
|
}
|
|
UniqueSEC_PKCS12ExportContext ecx(
|
|
SEC_PKCS12CreateExportContext(nullptr, nullptr, nullptr, nullptr));
|
|
if (!ecx) {
|
|
aError = nsIX509CertDB::ERROR_PKCS12_BACKUP_FAILED;
|
|
return NS_OK;
|
|
}
|
|
bool useModernCrypto =
|
|
StaticPrefs::security_pki_use_modern_crypto_with_pkcs12();
|
|
// add password integrity
|
|
SECItem unicodePw = {siBuffer, passwordBuffer.get(), passwordBufferLength};
|
|
SECStatus srv = SEC_PKCS12AddPasswordIntegrity(
|
|
ecx.get(), &unicodePw, useModernCrypto ? SEC_OID_SHA256 : SEC_OID_SHA1);
|
|
if (srv != SECSuccess) {
|
|
aError = nsIX509CertDB::ERROR_PKCS12_BACKUP_FAILED;
|
|
return NS_OK;
|
|
}
|
|
for (auto& cert : aCerts) {
|
|
UniqueCERTCertificate nssCert(cert->GetCert());
|
|
if (!nssCert) {
|
|
aError = nsIX509CertDB::ERROR_PKCS12_BACKUP_FAILED;
|
|
return NS_OK;
|
|
}
|
|
// 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.
|
|
UniqueSECKEYPrivateKey privKey(
|
|
PK11_FindKeyByDERCert(nssCert->slot, nssCert.get(), mUIContext));
|
|
if (privKey && !isExtractable(privKey)) {
|
|
// This is informative. If a serious error occurs later it will
|
|
// override it later and return.
|
|
aError = nsIX509CertDB::ERROR_PKCS12_NOSMARTCARD_EXPORT;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// certSafe and keySafe are owned by ecx.
|
|
SEC_PKCS12SafeInfo* certSafe;
|
|
SEC_PKCS12SafeInfo* keySafe = SEC_PKCS12CreateUnencryptedSafe(ecx.get());
|
|
// We use SEC_OID_AES_128_CBC for the password and SEC_OID_AES_256_CBC
|
|
// for the certificate because it's a default for openssl an pk12util
|
|
// command.
|
|
if (!SEC_PKCS12IsEncryptionAllowed() || PK11_IsFIPS()) {
|
|
certSafe = keySafe;
|
|
} else {
|
|
SECOidTag privAlg =
|
|
useModernCrypto ? SEC_OID_AES_128_CBC
|
|
: SEC_OID_PKCS12_V2_PBE_WITH_SHA1_AND_40_BIT_RC2_CBC;
|
|
certSafe =
|
|
SEC_PKCS12CreatePasswordPrivSafe(ecx.get(), &unicodePw, privAlg);
|
|
}
|
|
if (!certSafe || !keySafe) {
|
|
aError = nsIX509CertDB::ERROR_PKCS12_BACKUP_FAILED;
|
|
return NS_OK;
|
|
}
|
|
// add the cert and key to the blob
|
|
SECOidTag algorithm =
|
|
useModernCrypto
|
|
? SEC_OID_AES_256_CBC
|
|
: SEC_OID_PKCS12_V2_PBE_WITH_SHA1_AND_3KEY_TRIPLE_DES_CBC;
|
|
srv = SEC_PKCS12AddCertAndKey(ecx.get(), certSafe, nullptr, nssCert.get(),
|
|
CERT_GetDefaultCertDB(), keySafe, nullptr,
|
|
true, &unicodePw, algorithm);
|
|
if (srv != SECSuccess) {
|
|
aError = nsIX509CertDB::ERROR_PKCS12_BACKUP_FAILED;
|
|
return NS_OK;
|
|
}
|
|
}
|
|
|
|
UniquePRFileDesc prFile;
|
|
PRFileDesc* rawPRFile;
|
|
nsresult rv = aFile->OpenNSPRFileDesc(PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE,
|
|
0664, &rawPRFile);
|
|
if (NS_FAILED(rv) || !rawPRFile) {
|
|
aError = nsIX509CertDB::ERROR_PKCS12_BACKUP_FAILED;
|
|
return NS_OK;
|
|
}
|
|
prFile.reset(rawPRFile);
|
|
// encode and write
|
|
srv = SEC_PKCS12Encode(ecx.get(), writeExportFile, prFile.get());
|
|
if (srv != SECSuccess) {
|
|
aError = nsIX509CertDB::ERROR_PKCS12_BACKUP_FAILED;
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
// 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 nsAString& uni, uint32_t& bytesLength) {
|
|
if (uni.IsVoid()) {
|
|
bytesLength = 0;
|
|
return nullptr;
|
|
}
|
|
|
|
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(
|
|
buffer.get(), static_cast<const char16_t*>(uni.BeginReading()),
|
|
wideLength);
|
|
|
|
return buffer;
|
|
}
|
|
|
|
// Given a decoder, read bytes from file and input them to the decoder.
|
|
nsresult nsPKCS12Blob::inputToDecoder(UniqueSEC_PKCS12DecoderContext& dcx,
|
|
nsIFile* file, PRErrorCode& nssError) {
|
|
nssError = 0;
|
|
|
|
nsCOMPtr<nsIInputStream> fileStream;
|
|
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
|
|
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) {
|
|
break;
|
|
}
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
// What to do when the nickname collides with one already in the db.
|
|
SECItem* nsPKCS12Blob::nicknameCollision(SECItem* oldNick, PRBool* cancel,
|
|
void* wincx) {
|
|
*cancel = false;
|
|
int count = 1;
|
|
nsCString nickname;
|
|
nsAutoString nickFromProp;
|
|
nsresult rv = GetPIPNSSBundleString("P12DefaultNickname", nickFromProp);
|
|
if (NS_FAILED(rv)) {
|
|
return nullptr;
|
|
}
|
|
NS_ConvertUTF16toUTF8 nickFromPropC(nickFromProp);
|
|
// The user is trying to import a PKCS#12 file that doesn't have the
|
|
// attribute we use to set the nickname. So in order to reduce the
|
|
// number of interactions we require with the user, we'll build a nickname
|
|
// for the user. The nickname isn't prominently displayed in the UI,
|
|
// so it's OK if we generate one on our own here.
|
|
// XXX If the NSS API were smarter and actually passed a pointer to
|
|
// the CERTCertificate* we're importing we could actually just
|
|
// call default_nickname (which is what the issuance code path
|
|
// does) and come up with a reasonable nickname. Alas, the NSS
|
|
// API limits our ability to produce a useful nickname without
|
|
// bugging the user. :(
|
|
while (1) {
|
|
// If we've gotten this far, that means there isn't a certificate
|
|
// in the database that has the same subject name as the cert we're
|
|
// trying to import. So we need to come up with a "nickname" to
|
|
// satisfy the NSS requirement or fail in trying to import.
|
|
// Basically we use a default nickname from a properties file and
|
|
// see if a certificate exists with that nickname. If there isn't, then
|
|
// create update the count by one and append the string '#1' Or
|
|
// whatever the count currently is, and look for a cert with
|
|
// that nickname. Keep updating the count until we find a nickname
|
|
// without a corresponding cert.
|
|
// XXX If a user imports *many* certs without the 'friendly name'
|
|
// attribute, then this may take a long time. :(
|
|
nickname = nickFromPropC;
|
|
if (count > 1) {
|
|
nickname.AppendPrintf(" #%d", count);
|
|
}
|
|
UniqueCERTCertificate cert(
|
|
CERT_FindCertByNickname(CERT_GetDefaultCertDB(), nickname.get()));
|
|
if (!cert) {
|
|
break;
|
|
}
|
|
count++;
|
|
}
|
|
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;
|
|
|
|
return newNick.release();
|
|
}
|
|
|
|
// write bytes to the exported PKCS#12 file
|
|
void nsPKCS12Blob::writeExportFile(void* arg, const char* buf,
|
|
unsigned long len) {
|
|
PRFileDesc* file = static_cast<PRFileDesc*>(arg);
|
|
MOZ_RELEASE_ASSERT(file);
|
|
PR_Write(file, buf, len);
|
|
}
|
|
|
|
// Translate PRErrorCode to nsIX509CertDB error
|
|
uint32_t nsPKCS12Blob::handlePRErrorCode(PRErrorCode aPrerr) {
|
|
MOZ_ASSERT(aPrerr != 0);
|
|
uint32_t error = nsIX509CertDB::ERROR_UNKNOWN;
|
|
switch (aPrerr) {
|
|
case SEC_ERROR_PKCS12_CERT_COLLISION:
|
|
error = nsIX509CertDB::ERROR_PKCS12_DUPLICATE_DATA;
|
|
break;
|
|
// INVALID_ARGS is returned on bad password when importing cert
|
|
// exported from firefox or generated by openssl
|
|
case SEC_ERROR_INVALID_ARGS:
|
|
case SEC_ERROR_BAD_PASSWORD:
|
|
error = nsIX509CertDB::ERROR_BAD_PASSWORD;
|
|
break;
|
|
case SEC_ERROR_BAD_DER:
|
|
case SEC_ERROR_PKCS12_CORRUPT_PFX_STRUCTURE:
|
|
case SEC_ERROR_PKCS12_INVALID_MAC:
|
|
error = nsIX509CertDB::ERROR_DECODE_ERROR;
|
|
break;
|
|
case SEC_ERROR_PKCS12_DUPLICATE_DATA:
|
|
error = nsIX509CertDB::ERROR_PKCS12_DUPLICATE_DATA;
|
|
break;
|
|
}
|
|
return error;
|
|
}
|