When EV (Extended Validation) Code Signing Certificates are used, the customer receives a hardware token that must be inserted whenever the certificate is used. Also, a password must be entered each time. This article explains how to do the entire process programmatically and without having to enter the password each time.
Background
At Secured Globe, Inc. we use Comodo EV Code Signing Certificate for signing our products. Comodo sents a SafeNet eToken and other providers do the same. Similar products are Digicert's,
Kernel Mode Code Signing
Kernel Drivers signing requires Kernel Mode Code Signing. Such signing is done using cross certificates. In Windows, cross-certificates allow the operating system kernel to have a single trusted Microsoft root authority and extend the chain of trust to multiple commercial CAs that issue Software Publisher Certificates (SPCs), which are used for code-signing software for distribution, installation, and loading on Windows.
Stages of EV Code Signing
As explained in this blog post, EV Code Signing requires the following steps:
- Connect to the hardware token.
- Use a Root Certificate (for Kernel Mode Code Signing)
- Use of Cross Certificate (for Kernel Mode Code Signing)
- Code sign using the EV Certificate, the private key and the Root / Cross certificates.
Certificate Contexes
A certificate context contains both the encoded and decoded representation of a certificate. A certificate context returned by a cert store function must be freed by calling the CertFreeCertificateContext
function. The CertDuplicateCertificateContext
function can be called to make a duplicate copy (which also must be freed by calling CertFreeCertificateContext
).
typedef struct _CERT_CONTEXT
{
DWORD dwCertEncodingType;
BYTE *pbCertEncoded;
DWORD cbCertEncoded;
PCERT_INFO pCertInfo;
HCERTSTORE hCertStore;
} CERT_CONTEXT, *PCERT_CONTEXT;
typedef const CERT_CONTEXT *PCCERT_CONTEXT;
Opening a Token
When we need to access a Hardware Token and access it, we can do that programmatically.
We need to define two constants:
#define SAFENET_TOKEN L"\\\\.\\AKS ifdh 0"
#define EV_PASS "<your private key here"
The default name for a Safenet eToken would be "\\\\.\\AKS ifdh 0".
The second parameter would be your private key.
Then we call OpenToken
as follows:
PCCERT_CONTEXT cert = OpenToken(SAFENET_TOKEN, EV_PASS);
The returned value is PCCERT_CONTEXT
, which means we can then use it to code sign in a similar way we would have used it, had that been a regular Code Signing Certificate.
Here is the code for OpenToken
:
PCCERT_CONTEXT OpenToken(const std::wstring& TokenName, const std::string& TokenPin)
{
const wchar_t DefProviderName[] = L"eToken Base Cryptographic Provider";
HCRYPTPROV hProv = NULL;
if (!CryptAcquireContextW(&hProv, TokenName.c_str(),
DefProviderName, PROV_RSA_FULL, CRYPT_SILENT))
{
DWORD Error = GetLastError();
wprintf(L"Opening token %ws has failed while calling CryptAcquireContext,
error 0x%08X\n", TokenName.c_str(), Error);
MessageBox(NULL, L"You must insert the Token to your USB port", L"", MB_OK);
return NULL;
}
if (!CryptSetProvParam(hProv, PP_SIGNATURE_PIN, (BYTE*)TokenPin.c_str(), 0))
{
DWORD Error = GetLastError();
wprintf(L"Failed to unlock token %ws! Error 0x%08X\n", TokenName.c_str(), Error);
CryptReleaseContext(hProv, 0);
return NULL;
}
else
{
BOOL bStatus = FALSE;
DWORD dwErr = 0;
DWORD dwFlags = CRYPT_FIRST;
PCCERT_CONTEXT pContextArray[128];
DWORD dwContextArrayLen = 0;
HCRYPTKEY hKey = NULL;
LPBYTE pbCert = NULL;
DWORD dwCertLen = 0;
PCCERT_CONTEXT pCertContext = NULL;
DWORD pKeySpecs[2] = { AT_KEYEXCHANGE, AT_SIGNATURE };
wprintf(L"Successfully unlocked token %ws\n", TokenName.c_str());
bStatus = CryptAcquireContext(&hProv,
TokenName.c_str(),
DefProviderName,
PROV_RSA_FULL,
0);
if (!bStatus)
{
dwErr = GetLastError();
goto end;
}
if (CryptAcquireContext(&hProv,
TokenName.c_str(),
DefProviderName,
PROV_RSA_FULL,
0))
{
for (int i = 0; i < 2; i++)
{
if (CryptGetUserKey(hProv,
pKeySpecs[i],
&hKey))
{
if (CryptGetKeyParam(hKey,
KP_CERTIFICATE,
NULL,
&dwCertLen,
0))
{
pbCert = (LPBYTE)LocalAlloc(0, dwCertLen);
if (!pbCert)
{
dwErr = GetLastError();
goto end;
}
if (CryptGetKeyParam(hKey,
KP_CERTIFICATE,
pbCert,
&dwCertLen,
0))
{
pCertContext = CertCreateCertificateContext(
X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
pbCert,
dwCertLen);
if (pCertContext)
{
wprintf(L"Signing file\n");
pContextArray[dwContextArrayLen++] = pCertContext;
return pCertContext;
}
}
LocalFree(pbCert);
}
CryptDestroyKey(hKey);
hKey = NULL;
}
}
CryptReleaseContext(hProv, 0);
hProv = NULL;
}
dwFlags = 0;
end:
while (dwContextArrayLen--)
{
CertFreeCertificateContext(pContextArray[dwContextArrayLen]);
}
if (hKey)
CryptDestroyKey(hKey);
if (hProv)
CryptReleaseContext(hProv, 0);
return NULL;
}
}
Signing the File
The Windows API call we need to use is SignerSignEx2().
After we opened the certificate, either through the Hardware Token, or directly (non EV Code Signing Certificates), we call SignAppxPackage()
.
This function needs to be called using LoadLibrary() and GetProcAddress():
typedef HRESULT(WINAPI *SignerSignEx2Function)(
DWORD,
PSIGNER_SUBJECT_INFO,
PSIGNER_CERT,
PSIGNER_SIGNATURE_INFO,
PSIGNER_PROVIDER_INFO,
DWORD,
PCSTR,
PCWSTR,
PCRYPT_ATTRIBUTES,
PVOID,
PSIGNER_CONTEXT *,
PVOID,
PVOID);
HMODULE msSignModule = LoadLibraryEx(
L"MSSign32.dll",
NULL,
LOAD_LIBRARY_SEARCH_SYSTEM32);
if (msSignModule)
{
SignerSignEx2Function SignerSignEx2 = reinterpret_cast<SignerSignEx2Function>(
GetProcAddress(msSignModule, "SignerSignEx2"));
if (SignerSignEx2)
{
hr = SignerSignEx2(
signerParams.dwFlags,
signerParams.pSubjectInfo,
signerParams.pSigningCert,
signerParams.pSignatureInfo,
signerParams.pProviderInfo,
signerParams.dwTimestampFlags,
signerParams.pszAlgorithmOid,
signerParams.pwszTimestampURL,
signerParams.pCryptAttrs,
signerParams.pSipData,
signerParams.pSignerContext,
signerParams.pCryptoPolicy,
signerParams.pReserved);
}
else
{
DWORD lastError = GetLastError();
hr = HRESULT_FROM_WIN32(lastError);
}
FreeLibrary(msSignModule);
}
else
{
DWORD lastError = GetLastError();
hr = HRESULT_FROM_WIN32(lastError);
}
if (sipClientData.pAppxSipState)
{
sipClientData.pAppxSipState->Release();
}
Time Stamping
You must Time Stamp your signed file and do that using a Time Stamping authority to which you connect.
That is done by securely checking a Time Stamping server via URL for the current date and time. Each Signing authority have their own Time Stamping server. Time Stamping is an extra step in the Code Signing process, but when it comes to EV Code Signing, it is a requirement which adds an additional layer of security to the signed PE.
Preliminary Checks Before Signing
Before we can sign the PE, we check the following:
- Do we have a valid path to
CertAuthority_ROOT
? - Do we have a valid path to
CertAuthority_RSA
? - Do we have a valid path to
CROSSCERTPATH
? - Are we connected to the Internet? (we need Internet connection to properly timestamp the signed file).
Loading a Certificate from a File
Provided that we have a valid path for one of the certificates used in the process, we need to load it into memory. I wrote the following function for doing so.
std::tuple<DWORD, DWORD, std::string> GetCertificateFromFile
(const wchar_t* FileName
, std::shared_ptr<const CERT_CONTEXT>* ResultCert)
{
std::vector<unsigned char> vecAsn1CertBuffer;
auto tuple_result = ReadFileToVector(FileName, &vecAsn1CertBuffer);
if (std::get<0>(tuple_result) != 0)
{
return tuple_result;
}
return GetCertificateFromMemory(vecAsn1CertBuffer, ResultCert);
}
Here is the FormMemoryCertStore
function:
std::tuple<DWORD, DWORD, std::string> FormMemoryCertStore
(const std::vector<std::shared_ptr<const CERT_CONTEXT> >& Certs
, DWORD FlagsForAddCertToStore
, std::shared_ptr<void>* ResultStore)
{
HCERTSTORE hTmpMemoryStore = ::CertOpenStore(CERT_STORE_PROV_MEMORY, 0, NULL, 0, NULL);
if (hTmpMemoryStore == NULL)
{
return std::make_tuple(E_FAIL, ::GetLastError(), "CertOpenStore(Memory Store)");
}
for (unsigned int i = 0; i < Certs.size(); ++i)
{
int Result = ::CertAddCertificateContextToStore(hTmpMemoryStore
, Certs[i].get()
, FlagsForAddCertToStore
, NULL);
if (Result == 0)
{
DWORD dwLastError = ::GetLastError();
::CertCloseStore(hTmpMemoryStore, 0);
return std::make_tuple(E_FAIL, dwLastError, "CertAddCertificateContextToStore");
}
}
*ResultStore = std::shared_ptr<void>
(hTmpMemoryStore, std::bind(::CertCloseStore, std::placeholders::_1, 0));
return std::make_tuple(0, 0, "");
}
Here is the ReadFileToVector
function:
std::tuple<DWORD, DWORD, std::string> ReadFileToVector
(const std::wstring& FileName
, std::vector<unsigned char>* ResultData)
{
HANDLE hFile = ::CreateFile
(FileName.c_str(), GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
return std::make_tuple(E_FAIL, ::GetLastError(),
"CreateFile(GENERIC_READ, OPEN_EXISTING,)");
}
std::shared_ptr<void> sptrTemp(hFile, ::CloseHandle);
DWORD dwDataLen = ::GetFileSize(hFile, NULL);
if (dwDataLen == INVALID_FILE_SIZE)
{
return std::make_tuple(E_FAIL, ::GetLastError(), "GetFileSize");
}
ResultData->resize(dwDataLen);
DWORD dwNumberOfReadData = 0;
DWORD Result = ::ReadFile(hFile, &(*ResultData)[0],
static_cast<DWORD>(ResultData->size()), &dwNumberOfReadData, NULL);
if (Result == 0)
{
return std::make_tuple(E_FAIL, ::GetLastError(), "ReadFile");
}
return std::make_tuple(0, 0, "");
}
Loading a Certificate from Memory
After we load a certificate from a file, we need to load it into memory. That is done using the following function:
std::tuple<DWORD, DWORD, std::string> GetCertificateFromMemory
(const std::vector<unsigned char>& CertData
, std::shared_ptr<const CERT_CONTEXT>* ResultCert)
{
const CERT_CONTEXT* crtResultCert = ::CertCreateCertificateContext
(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING
, &CertData[0]
, static_cast<DWORD>(CertData.size()));
if (crtResultCert == NULL)
{
return std::make_tuple(E_FAIL
, ::GetLastError()
, "CertCreateCertificateContext");
}
*ResultCert = std::shared_ptr<const CERT_CONTEXT>(crtResultCert
, ::CertFreeCertificateContext);
return std::make_tuple(0, 0, "");
}
As shown in this article, the certificate embedded in the Hardware Token was loaded from the Token into memory. Now, here is how we access it:
std::vector<unsigned char> dataCertEV(signingCertContext->pbCertEncoded,
signingCertContext->pbCertEncoded + signingCertContext->cbCertEncoded);
The SignAppxPackage Function
Now, we are ready to use the SignAppxPackge()
function which goes as follows:
HRESULT SignAppxPackage(
_In_ PCCERT_CONTEXT signingCertContext,
_In_ LPCWSTR packageFilePath)
{
HRESULT hr = S_OK;
if (PathFileExists(CertAuthority_ROOT))
{
wprintf(L"Cross Certificate '%s' was found\n", CertAuthority_ROOT);
}
else
{
wprintf(L"Error: Cross Certificate '%s' was not found\n", CertAuthority_ROOT);
return 3;
}
DWORD dwReturnedFlag;
if (InternetGetConnectedState(&dwReturnedFlag,0) == NULL)
{
wprintf(L"Certificate can't be dated with no Internet connection\n");
return 1;
}
if (PathFileExists(CertAuthority_RSA))
{
wprintf(L"Cross Certificate '%s' was found\n", CertAuthority_RSA);
}
else
{
wprintf(L"Error: Cross Certificate '%s' was not found\n", CertAuthority_RSA);
return 2;
}
if (PathFileExists(CROSSCERTPATH))
{
wprintf(L"Microsoft Cross Certificate '%s' was found\n", CROSSCERTPATH);
}
else
{
wprintf(L"Error: Microsoft Cross Certificate '%s' was not found\n", CROSSCERTPATH);
return 3;
}
DWORD signerIndex = 0;
SIGNER_FILE_INFO fileInfo = {};
fileInfo.cbSize = sizeof(SIGNER_FILE_INFO);
fileInfo.pwszFileName = packageFilePath;
SIGNER_SUBJECT_INFO subjectInfo = {};
subjectInfo.cbSize = sizeof(SIGNER_SUBJECT_INFO);
subjectInfo.pdwIndex = &signerIndex;
subjectInfo.dwSubjectChoice = SIGNER_SUBJECT_FILE;
subjectInfo.pSignerFileInfo = &fileInfo;
SIGNER_CERT_STORE_INFO certStoreInfo = {};
certStoreInfo.cbSize = sizeof(SIGNER_CERT_STORE_INFO);
certStoreInfo.dwCertPolicy =
SIGNER_CERT_POLICY_STORE; certStoreInfo.pSigningCert = signingCertContext;
auto fileCertAuthorityRsaEVCA = CertAuthority_RSA;
std::shared_ptr<const CERT_CONTEXT> certCertAuthorityRsaEVCA;
auto tuple_result = GetCertificateFromFile
(fileCertAuthorityRsaEVCA, &certCertAuthorityRsaEVCA);
if (std::get<0>(tuple_result) != 0)
{
std::cout << "Error: " << std::get<0>(tuple_result)
<< " " << std::get<1>(tuple_result) << " " << std::get<2>(tuple_result) << "\n";
return std::get<0>(tuple_result);
}
std::shared_ptr<const CERT_CONTEXT> certCertEV;
std::vector<unsigned char> dataCertEV(signingCertContext->pbCertEncoded,
signingCertContext->pbCertEncoded + signingCertContext->cbCertEncoded);
tuple_result = GetCertificateFromMemory(dataCertEV, &certCertEV);
if (std::get<0>(tuple_result) != 0)
{
std::cout << "Error: " << std::get<0>(tuple_result) << " "
<< std::get<1>(tuple_result) << " " << std::get<2>(tuple_result) << "\n";
return std::get<0>(tuple_result);
}
auto fileCertCross = CertAuthority_ROOT;
std::shared_ptr<const CERT_CONTEXT> certCertCross;
tuple_result = GetCertificateFromFile(fileCertCross, &certCertCross);
if (std::get<0>(tuple_result) != 0)
{
std::cout << "Error: " << std::get<0>(tuple_result) << " "
<< std::get<1>(tuple_result) << " " << std::get<2>(tuple_result) << "\n";
return std::get<0>(tuple_result);
}
std::vector<std::shared_ptr<const CERT_CONTEXT> > certs;
certs.push_back(certCertAuthorityRsaEVCA);
certs.push_back(certCertEV);
certs.push_back(certCertCross);
std::shared_ptr<void> resultStore;
tuple_result = FormMemoryCertStore(certs, CERT_STORE_ADD_NEW, &resultStore);
if (std::get<0>(tuple_result) != 0)
{
std::cout << "Error: " << std::get<0>(tuple_result) << " "
<< std::get<1>(tuple_result) << " " << std::get<2>(tuple_result) << "\n";
return std::get<0>(tuple_result);
}
certStoreInfo.hCertStore = resultStore.get();
SIGNER_CERT cert = {};
cert.cbSize = sizeof(SIGNER_CERT);
cert.dwCertChoice = SIGNER_CERT_STORE;
cert.pCertStoreInfo = &certStoreInfo;
SIGNER_SIGNATURE_INFO signatureInfo = {};
signatureInfo.cbSize = sizeof(SIGNER_SIGNATURE_INFO);
signatureInfo.algidHash = CALG_SHA_256;
signatureInfo.dwAttrChoice = SIGNER_NO_ATTR;
SIGNER_SIGN_EX2_PARAMS signerParams = {};
signerParams.pSubjectInfo = &subjectInfo;
signerParams.pSigningCert = &cert;
signerParams.pSignatureInfo = &signatureInfo;
signerParams.dwTimestampFlags = SIGNER_TIMESTAMP_RFC3161;
signerParams.pszAlgorithmOid = szOID_NIST_sha256;
signerParams.pwszTimestampURL = TIMESTAMPURL;
APPX_SIP_CLIENT_DATA sipClientData = {};
sipClientData.pSignerParams = &signerParams;
signerParams.pSipData = &sipClientData;
typedef HRESULT(WINAPI *SignerSignEx2Function)(
DWORD,
PSIGNER_SUBJECT_INFO,
PSIGNER_CERT,
PSIGNER_SIGNATURE_INFO,
PSIGNER_PROVIDER_INFO,
DWORD,
PCSTR,
PCWSTR,
PCRYPT_ATTRIBUTES,
PVOID,
PSIGNER_CONTEXT *,
PVOID,
PVOID);
HMODULE msSignModule = LoadLibraryEx(
L"MSSign32.dll",
NULL,
LOAD_LIBRARY_SEARCH_SYSTEM32);
if (msSignModule)
{
SignerSignEx2Function SignerSignEx2 = reinterpret_cast<SignerSignEx2Function>(
GetProcAddress(msSignModule, "SignerSignEx2"));
if (SignerSignEx2)
{
hr = SignerSignEx2(
signerParams.dwFlags,
signerParams.pSubjectInfo,
signerParams.pSigningCert,
signerParams.pSignatureInfo,
signerParams.pProviderInfo,
signerParams.dwTimestampFlags,
signerParams.pszAlgorithmOid,
signerParams.pwszTimestampURL,
signerParams.pCryptAttrs,
signerParams.pSipData,
signerParams.pSignerContext,
signerParams.pCryptoPolicy,
signerParams.pReserved);
}
else
{
DWORD lastError = GetLastError();
hr = HRESULT_FROM_WIN32(lastError);
}
FreeLibrary(msSignModule);
}
else
{
DWORD lastError = GetLastError();
hr = HRESULT_FROM_WIN32(lastError);
}
if (sipClientData.pAppxSipState)
{
sipClientData.pAppxSipState->Release();
}
return hr;
}
So provided that FILETOSIGN
contains the PE name we wish to sign, our entire code signing process consists of the following function calls:
PCCERT_CONTEXT cert = OpenToken(SAFENET_TOKEN, EV_PASS);
HRESULT hr = SignAppxPackage(cert, FILETOSIGN);
Submitting Kernel Drivers to Microsoft Labs
Kernel Drivers needs to be EV code signed and cross signed as described in this article. Then, you need to pack the drivers' files into a cab file and sign it as well. Here is an article I wrote, explaining how to do that as well.
Points of Interest
History
- 15th December, 2018: Initial version