Creating a temporary client certificate (including a private key) - c++

As part of an application I am writing, I wish for my program to temporarily install a certificate on the local machine and for WinHTTP to use it as the client certificate when connecting to a web server. The aim of this is help protect the web server from unauthorised access (this certificate is only a layer of the security - I know someone could extract it from the .exe). I do not want the user to have to install the certificate and I do not want the certificate to be left on the PC when the application is not running.
At the moment, I'm trying this:
Install the certificate manually from a .p12 file
Use a C++ application to get the binary data out of the local certificate and into a C array in my application (using CryptExportKey and PCCERT_CONTEXT::pbCertEncoded)
Uninstall the certificate
When the application boots:
Open a temporary store for the certificate
m_certificateStoreHandle = CertOpenStore( CERT_STORE_PROV_MEMORY, 0, NULL, 0, NULL );
Call CertAddEncodedCertificateToStore to add the certificate
CertAddEncodedCertificateToStore( m_certificateStoreHandle,
X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
reinterpret_cast< const BYTE * >( certificateData ),
dataSize,
CERT_STORE_ADD_REPLACE_EXISTING,
&m_clientCertificate )
Call CryptAcquireContext to get somewhere to store the private key (I change the name of the key on each run for the moment - ideally I plan to use CRYPT_VERIFYCONTEXT to make the key non-persistant, but that's something to ignore for now)
HCRYPTPROV cryptProvider = NULL;
HCRYPTKEY cryptKey = NULL;
CryptAcquireContext( &cryptProvider, "MyTestKeyNumber123", NULL, PROV_RSA_FULL, CRYPT_NEWKEYSET )
Call CryptImportKey to load the private key into the key store
CryptImportKey( cryptProvider, reinterpret_cast< BYTE * >( privateKey ), keySize, 0, CRYPT_EXPORTABLE, &cryptKey )
Call CertSetCertificateContextProperty to link the certificate to the private key
char containerName[128];
DWORD containerNameSize = ARRAY_NUM_BYTES(containerName);
char providerName[128];
DWORD providerNameSize = ARRAY_NUM_BYTES(providerName);
CryptGetProvParam(cryptProvider, PP_CONTAINER, reinterpret_cast<byte *>(containerName), &containerNameSize, 0)
CryptGetProvParam(cryptProvider, PP_NAME, reinterpret_cast<byte *>(providerName), &providerNameSize, 0)
WCHAR containerNameWide[128];
convertCharToWChar(containerNameWide, containerName);
WCHAR providerNameWide[128];
convertCharToWChar(providerNameWide, providerName);
CRYPT_KEY_PROV_INFO privateKeyData;
neMemZero(&privateKeyData, sizeof(privateKeyData));
privateKeyData.pwszContainerName = containerNameWide;
privateKeyData.pwszProvName = providerNameWide;
privateKeyData.dwProvType = 0;
privateKeyData.dwFlags = CRYPT_SILENT;
privateKeyData.dwKeySpec = AT_KEYEXCHANGE;
if ( CertSetCertificateContextProperty( m_clientCertificate, CERT_KEY_PROV_INFO_PROP_ID, 0, &privateKeyData ) )
Verify I can access the private key (Works, outputs exactly the same data as I have hardcoded into the application)
byte privateKeyBuffer[2048];
DWORD privateKeyBufferSize = ARRAY_NUM_BYTES(privateKeyBuffer);
memZero(privateKeyBuffer, privateKeyBufferSize);
if(CryptExportKey(cryptKey, 0, PRIVATEKEYBLOB, 0, privateKeyBuffer, &privateKeyBufferSize))
{
TRACE("Got private key!");
LOG_BUFFER(privateKeyBuffer, privateKeyBufferSize);
}
Attempt to verify the client certificate works as expected
char certNameBuffer[128] = "";
char certUrlBuffer[128] = "";
CertGetNameString(testValue, CERT_NAME_FRIENDLY_DISPLAY_TYPE, 0, NULL, certNameBuffer, ARRAY_NUM_BYTES(certNameBuffer));
CertGetNameString(testValue, CERT_NAME_URL_TYPE , 0, NULL, certUrlBuffer, ARRAY_NUM_BYTES(certUrlBuffer));
TRACE("SSL Certificate %s [%s]", certNameBuffer, certUrlBuffer);
HCRYPTPROV_OR_NCRYPT_KEY_HANDLE privateKey;
DWORD privateKeyType;
BOOL freeKeyAfter = false;
if(CryptAcquireCertificatePrivateKey(testValue, CRYPT_ACQUIRE_NO_HEALING, NULL, &privateKey, &privateKeyType, &freeKeyAfter))
{
HCRYPTPROV privateKeyProvider = static_cast<HCRYPTPROV>(privateKey);
HCRYPTKEY privateKeyHandle;
if(CryptGetUserKey(privateKeyProvider, privateKeyType, &privateKeyHandle))
{
NEbyte privateKeyBuffer[2048];
DWORD privateKeyBufferSize = NE_ARRAY_NUM_BYTES(privateKeyBuffer);
neMemZero(privateKeyBuffer, privateKeyBufferSize);
if(CryptExportKey(privateKeyHandle, 0, PRIVATEKEYBLOB, 0, privateKeyBuffer, &privateKeyBufferSize))
{
NE_TRACE("Got private key!");
HTTP_LOG_BUFFER(neGetGlobalTraceLog(), "Key", "", privateKeyBuffer, privateKeyBufferSize);
}
At this stage, the private key is found but the call to CryptExportKey fails with NTE_BAD_KEY_STATE. When I try to use the client certificate with WinHTTP, I get ERROR_WINHTTP_CLIENT_CERT_NO_ACCESS_PRIVATE_KEY. If anyone is wondering, I tell WinHTTP to use the client certificate with this code:
if ( !WinHttpSetOption( handle,
WINHTTP_OPTION_CLIENT_CERT_CONTEXT,
const_cast<PCERT_CONTEXT>(m_clientCertificate),
sizeof( CERT_CONTEXT ) ) )
{
HTTP_LOG_ERROR( getLog(), "Setting the client certificate failed with error code %x", GetLastError() );
}
The way I see it, until I can somehow link the private key and the certificate together and make it so that I can use CryptAcquireCertificatePrivateKey with CryptExportKey to get the key data back out, WinHTTP doesn't stand a chance of being succesful.
Any thoughts as to why I can't seem to get my certificate to use the private key?

I didn't manage to get this approach working. Instead, I ended up using PFXImportCertStore along with the raw .p12 file containing the certificate and private key I wanted to use.
From what I've seen, it seems that PFXImportCertStore will create a new store in memory. The only thing I haven't been able to find out is if the private key is also stored in memory or if it ends up permenently on the PC somewhere. If I find out either way, I'll update this answer.
m_clientCertificateStoreHandle = PFXImportCertStore(&pfxData, certificatePassword, 0);
if(NULL != m_clientCertificateStoreHandle)
{
m_clientCertificateHandle = CertFindCertificateInStore( m_clientCertificateStoreHandle, X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, 0, CERT_FIND_ANY, NULL, NULL );
}

Related

Getting handles to the credentials of a domain user from a process launched by a local user on Windows 10

Background:
Computer mycomputer is running Windows 10 and is joined to domain mydomain.com.
A user is logged with local account mycomputer\localuser on mycomputer.
The user also knows the password of domain account mydomain\domainuser.
The service principal name myprotocol/domainuser is registered in Active Directory and maps to domain account mydomain\domainuser.
Local user mycomputer\localuser is not allowed to start a process as mydomain\domainuser.
The user wants to launch a server process under the local account which would then use the domain account to authenticate incoming connections with Kerberos.
I want to write the code of that server.
Client code:
The client code is straightforward and consist of a call to AcquireCredentialsHandle followed by a call to InitializeSecurityContext:
AcquireCredentialsHandle(
nullptr,
"Kerberos",
SECPKG_CRED_OUTBOUND,
nullptr,
nullptr,
nullptr,
nullptr,
&credentials,
&lifetime);
InitializeSecurityContext(
&credentials,
nullptr,
"myprotocol/myport",
ISC_REQ_CONFIDENTIALITY,
0,
SECURITY_NATIVE_DREP,
nullptr,
0,
&securityContext,
&outBufferArray,
&contextAttributes,
&lifetime);
Note the simplified usage of strings in the code snippets. The reality which have to deal with wchar_t and const correctness is somewhat uglier.
Also note that this code works when launched by a local user if appropriate credentials are stored in Control Panel's Credential Manager - i.e. with hostname domainuser (sic.)
Server code:
I already have a code which works when the process is launched by mydomain\domainuser:
AcquireCredentialsHandle(
nullptr,
"Kerberos",
SECPKG_CRED_INBOUND,
nullptr,
nullptr,
nullptr,
nullptr,
&credentials,
&lifetime);
AcceptSecurityContext(
&credentials,
nullptr,
&inBufferArray,
attribs,
SECURITY_NATIVE_DREP,
&securityContext,
nullptr,
&attribs,
&lifetime);
But when the server is launched by mycomputer\localuser, the call to AcquireCredentialsHandle fails with code SEC_E_NO_CREDENTIALS.
I tried modifying the first argument of that call to "myprotocol/domainuser", "domainuser", "mydomain\domainuser" or even "domainuser#mydomain.com".
I tried adding the required credentials in Control Panel's Credential Manager using hostname mycomputer and even domainuser.
What can I do to acquire the credential handle of mydomain\domainuser in a process launched by mycomputer\localuser ?
Compiling code snippet:
#include <string>
#define NOMINMAX
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#define SECURITY_WIN32
#include <sspi.h>//Requires linking on Secur32.lib
int main(){
CredHandle credentials;
TimeStamp lifetime;
std::string package="Kerberos";
std::string principal="myprotocol/domainuser";
auto res=AcquireCredentialsHandle(
principal.data(),
package.data(),
SECPKG_CRED_INBOUND,
nullptr,
nullptr,
nullptr,
nullptr,
&credentials,
&lifetime);
if(res==SEC_E_OK){
std::printf("Success\n");
FreeCredentialsHandle(&credentials);
return 0;}
else{
std::printf("Failure\n");
return res;}}
To obtain credentials other than those associated with the current logon session, populate a SEC_WINNT_AUTH_IDENTITY structure with information for the alternate security principal. Pass the structure to the AcquireCredentialsHandle function using the pAuthData parameter.
And this micrsoft example demonstrates a client-side call to obtain Digest credentials for a specific user account:
#include <windows.h>
#ifdef UNICODE
ClientAuthID.Flags = SEC_WINNT_AUTH_IDENTITY_UNICODE;
#else
ClientAuthID.Flags = SEC_WINNT_AUTH_IDENTITY_ANSI;
#endif
void main()
{
SECURITY_STATUS SecStatus;
TimeStamp tsLifetime;
CredHandle hCred;
SEC_WINNT_AUTH_IDENTITY ClientAuthID;
LPTSTR UserName = TEXT("ASecurityPrinciple");
LPTSTR DomainName = TEXT("AnAuthenticatingDomain");
// Initialize the memory.
ZeroMemory( &ClientAuthID, sizeof(ClientAuthID) );
// Specify string format for the ClientAuthID structure.
// Specify an alternate user, domain and password.
ClientAuthID.User = (unsigned char *) UserName;
ClientAuthID.UserLength = _tcslen(UserName);
ClientAuthID.Domain = (unsigned char *) DomainName;
ClientAuthID.DomainLength = _tcslen(DomainName);
// Password is an application-defined LPTSTR variable
// containing the user password.
ClientAuthID.Password = Password;
ClientAuthID.PasswordLength = _tcslen(Password);
// Get the client side credential handle.
SecStatus = AcquireCredentialsHandle (
NULL, // Default principal.
WDIGEST_SP_NAME, // The Digest SSP.
SECPKG_CRED_OUTBOUND, // Client will use the credentials.
NULL, // Do not specify LOGON id.
&ClientAuthID, // User information.
NULL, // Not used with Digest SSP.
NULL, // Not used with Digest SSP.
&hCred, // Receives the credential handle.
&tsLifetime // Receives the credential time limit.
);
}

Will this digital signing code work with a Qualified Digital Certificate?

I was recently asked if our existing signing code will work with a European Qualified Digital Certificate in place of the RSA signing certificate we normally use. As far as I can tell it should, but I haven't found anything that actually confirms this theory.
Short of figuring out how to acquire a qualified certificate and actually testing it, I'm not sure how to answer this question definitively. Anybody happen to have experience with this?
The code in question is shown below. It's a Windows-based C++ application that is using Microsoft's Cryptographic API for signing.
int SignData(
const std::string &data, // Data to be signed
const char *containerName, // Name of key container to use
std::string &signature) // Returns the signature
{
HCRYPTPROV hProv = NULL;
HCRYPTHASH hHash = NULL;
HCRYPTKEY hKey = NULL;
DWORD dwLength;
int status = 0;
// Attempt to open the key container as a LocalMachine key.
if (CryptAcquireContext(
&hProv,
containerName,
NULL,
PROV_RSA_FULL,
CRYPT_MACHINE_KEYSET | CRYPT_SILENT))
{
// Create a SHA-1 hash context.
if (CryptCreateHash(hProv, CALG_SHA1, 0, 0, &hHash))
{
// Calculate hash of the buffer.
if (CryptHashData(hHash, (const BYTE *)data.data(), (DWORD)data.size(), 0))
{
// Determine the size of the signature and allocate memory.
if (CryptSignHash(hHash, AT_SIGNATURE, 0, 0, NULL, &dwLength))
{
signature.resize(dwLength);
// Sign the hash object.
if (!CryptSignHash(hHash, AT_SIGNATURE, 0, 0, (BYTE*)&signature[0], &dwLength))
status = HandleCryptError("CryptSignHash failed");
}
else
status = HandleCryptError("CryptSignHash failed");
}
else
status = HandleCryptError("CryptHashData failed");
}
else
status = HandleCryptError("CryptCreateHash failed");
}
else
status = HandleCryptError("CryptAcquireContext failed");
return status;
}
The above code is using the default Microsoft software cryptographic provider which doesn't meet the requirements of eIDAS.
If you replace the default Microsoft provider by an eIDAS compliant provider in the above code then this approach of performing signature is OK. Under the hood, the compliant CSP will take care of non-repudiation requirement of the key usage, for example by displaying a PIN popup or if the key is stored remotely in a server by sending a 2FA notification to the key owner.

AcquireCredentialsHandle fails in kernel mode, when using SCH_CRED_FORMAT_CERT_HASH

I call AcquireCredentialsHandle in a kernel driver, passing in SCHANNEL_CRED with the dwCredFormat set to SCH_CRED_FORMAT_CERT_HASH. It fails with SEC_E_NO_CREDENTIALS. Here is my code:
BYTE certHashBytes[20] = { 0x6d,0x64,0xed,0x56,0xd2,0x94,0x15,0xf4,0x49,0x08,0xaf,0x18,0xf1,0xca,0xf5,0xa2,0xc8,0x01,0x20,0x96 };
CredHandle credHandle;
RtlZeroMemory(&credHandle, sizeof(CredHandle));
SCHANNEL_CRED schannelCred;
RtlZeroMemory(&schannelCred, sizeof(SCHANNEL_CRED));
schannelCred.dwVersion = 4;
schannelCred.cCreds = 1;
schannelCred.paCred = certHashBytes;
schannelCred.dwCredFormat = 1;
UNICODE_STRING unispName;
RtlUnicodeStringInit(&unispName, L"Microsoft Unified Security Protocol Provider");
TimeStamp ts;
SECURITY_STATUS res = AcquireCredentialsHandle(NULL, &unispName, SECPKG_CRED_INBOUND, NULL, &schannelCred, NULL, NULL, &credHandle, &ts);
DbgPrintEx(DPFLTR_IHVNETWORK_ID, DPFLTR_INFO_LEVEL, "AcquireCredentialsHandle %x\n", res);
My certificate hash is definitely correct, and installed properly in the MY store, for both User Account and Local Machine. I know this because it works fine in user mode, as follows:
HCERTSTORE certStore = CertOpenSystemStore(NULL, L"MY");
BYTE certHashBytes[20] = { 0x6d,0x64,0xed,0x56,0xd2,0x94,0x15,0xf4,0x49,0x08,0xaf,0x18,0xf1,0xca,0xf5,0xa2,0xc8,0x01,0x20,0x96 };
CERT_NAME_BLOB certHash { 20, certHashBytes };
PCCERT_CONTEXT cert = CertFindCertificateInStore(certStore, X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, 0, CERT_FIND_SHA1_HASH, &certHash, NULL);
CredHandle credHandle;
ZeroMemory(&credHandle, sizeof(CredHandle));
SCHANNEL_CRED cred;
ZeroMemory(&cred, sizeof(SCHANNEL_CRED));
cred.dwVersion = SCHANNEL_CRED_VERSION;
cred.cCreds = 1;
cred.paCred = &cert;
SECURITY_STATUS res = AcquireCredentialsHandle(NULL, const_cast<LPWSTR>(UNISP_NAME), SECPKG_CRED_INBOUND, NULL, &cred, NULL, NULL, &credHandle, NULL);
I believe I followed the MSDN instructions on how to use SCH_CRED_FORMAT_CERT_HASH exactly - what's wrong?
It is difficult to know for sure without debugging, however I see some points that could be the problem:
- If the certificate chain cannot be verified; or it is self-signed; or the machine does not have access to the internet at the time your code execute to check CRL, your call will fail. If this is the case, use CRYPT_E_NO_REVOCATION_CHECK
- If the purposes of your certificate are correct for proving an identity to remote servers?
There are some recent security hardenings in Windows to be very picky when it comes to certificates. A self-signed certificate sometimes is easier to test than a signed one. I have seen an increasing number of applications that were working stopped working for having a certificate not 100% proved. Short of it, I do not see what the problem is.

How can an independent DLL find out what Token file accesses will be made with?

If the parent process has used LogonUser so that the access token being used for file access is different than than the token the process was started with, how can the DLL find out the NT User name that the file accesses will be processed under?
If I had a specific file location then I could use GetFileSecurity, however I don't know any guaranteed accessible paths in the context of the DLL.
If I used the following:
PSID ownedSID(NULL);
SECURITY_INFORMATION siRequested = OWNER_SECURITY_INFORMATION;
wSecInfoOK = GetSecurityInfo(GetCurrentProcess(), SE_KERNEL_OBJECT, siRequested, &ownedSID, NULL, NULL, NULL, NULL);
then the PSID returned references the Windows user of the logged on process rather than that under which any writes will be treated as!
New Question in light comment / answer from #arx
I am now using TokenUser with GetTokenInformation on the handle from OpenThreadToken, but again I am getting the launching user but not the impersonated user
HANDLE hThreadToken = NULL;
if (OpenThreadToken(GetCurrentThread(), TOKEN_ALL_ACCESS, TRUE, &hThreadToken))
{
// success
CHeapPtr<TOKEN_USER, CGlobalAllocator> pToken;
DWORD length = 0U, dwError(0UL);
if (!GetTokenInformation(hThreadToken, TokenUser, NULL, 0, &length) && ERROR_INSUFFICIENT_BUFFER == GetLastError())
{
pToken.AllocateBytes(length);
SetLastError(ERROR_SUCCESS);//Reset last error - we have now allocated the required memory so the buffer is now big enough i.e GetLastError() != ERROR_INSUFFICIENT_BUFFER
if (pToken && GetTokenInformation(hThreadToken, TokenUser, pToken, length, &length))
{
if (IsValidSid(pToken->User.Sid))
sFailedUser = WinSecurityInfo::GetAccountSID(pToken->User.Sid, dwError);
}
dwError = GetLastError();
if (dwError)
{
boost::system::error_code sidError = MakeSysError(dwError);
TRACE("Error text for GetLastError() = '%s'\n", sidError.message().c_str());
}
}
}
P.S WinSecurityInfo::GetAccountSID is just a wrapper around LookupAccountSid
P.P.S Tried both FALSE and TRUE in OpenThreadToken, no change
You are looking at the wrong information in the thread token retrieved with OpenThreadToken. To get the identity of the user being impersonated you need to look at the TokenUser, not the TokenOwner.
Use GetTokenInformation to retrieve the user.
However, rather than going to great lengths to work in the face of impersonation, it is more usual to specify as part of your API contract that you don't. And then ignore the problem.

How to use X509_verify()

How can we use X509_verify(). I have two certificates. The first certificate is the Root Certificate which signed the next certificate (which is my Certificate). So I want to check if my certificate is signed by the root certificate using x509_verify() in C++. My goal is to keep the code simple and Understandable so I can also put it online.
Signature of X509_verify is
int X509_verify(X509 * x509, EVP_PKEY * pkey);
Suppose of you have root certificate in root and your certificate in mycert;
X509 * root;
X509 * mycert;
//Get root certificate into root
//Get mycert into mycert.
//Get the public key.
EVP_PKEY * pubkey = X509_get_pubkey(root);
//verify. result less than or 0 means not verified or some error.
int result = X509_verify(mycert, pubkey);
//free the public key.
EVP_PKEY_free(pubkey);
I think this would help you.
I think dbasic and Balamurugan answered how to use it. Here's how to interpret the errors you get from it. I find error handling is much more important than business logic because nearly anyone can copy/paste code that works in a benign environment. How you respond to failures, broken/bad inputs and a hostile environments matter more.
The source code for the function is in <openssl dir>/crypto/x509/x_all.c:
int X509_verify(X509 *a, EVP_PKEY *r)
{
return(ASN1_item_verify(ASN1_ITEM_rptr(X509_CINF),a->sig_alg,
a->signature,a->cert_info,r));
}
ASN1_item_verify id defined in <openssl dir>/crypto/asn1/a_verify.c:
int ASN1_item_verify(const ASN1_ITEM *it, X509_ALGOR *alg,
ASN1_BIT_STRING *signature, void *asn, EVP_PKEY *pkey)
{
...
}
The function returns -1 on failure with one of the following error codes:
ERR_R_PASSED_NULL_PARAMETER if pkey is NULL
ASN1_R_UNKNOWN_SIGNATURE_ALGORITHM if alg is unknown using an OID lookup
ASN1_R_WRONG_PUBLIC_KEY_TYPE if the pkey type does not match the pkey->method
ERR_R_MALLOC_FAILURE if a buffer allocation fails
The function returns 0 on failure with one of the following error codes:
ERR_R_EVP_LIB if EVP_DigestVerifyInit fails
ERR_R_EVP_LIB if EVP_DigestVerifyUpdate fails
ERR_R_EVP_LIB if EVP_DigestVerifyFinal fails
On success, the function returns 1 (from around line 220):
...
if (EVP_DigestVerifyFinal(&ctx,signature->data,
(size_t)signature->length) <= 0)
{
ASN1err(ASN1_F_ASN1_ITEM_VERIFY,ERR_R_EVP_LIB);
ret=0;
goto err;
}
ret=1;
err:
EVP_MD_CTX_cleanup(&ctx);
return(ret);
} /* End of function */
From <openssl dir>/crypto/err/err.h, you use ERR_get_error() to retrieve the error code:
err.h:#define ASN1err(f,r) ERR_PUT_error(ERR_LIB_ASN1,(f),(r),__FILE__,__LINE__)
Step1 : Read the certificate and convert the Certificate into X509 structure
// the below will show how to read the certificate from the file (DER or PEM Encoded)
X509* oCertificate=NULL;
FILE *lFp=NULL;
lFp=fopen(iFilePath.c_str(),"rb"); // iFilepath is the string
if(lFp==NULL)
{
oCertificate=NULL;
}
else
{
oCertificate = PEM_read_X509(lFp, NULL, NULL, NULL);
if (oCertificate == NULL )
{
//Certificate may be DER encode
oCertificate = d2i_X509_fp(lFp, NULL);
}
fclose(lFp);
}
// OCertificate contains
Step 2: now read the Root certificate key
(Note check the X509 is NULL or not before use)
Step 3 : use the X509_verify() function.