I need to mark a custom self-signed root certificate as trusted during certificate chain validation and, overall, I want to rely on the system API as much as possible.
I create a temporary memory store.
HCERTSTORE certStore = CertOpenStore(CERT_STORE_PROV_MEMORY, 0, 0, 0, 0);
Then I place the custom root certificate into the store.
CertAddCertificateContextToStore(certStore, rootCertContext, CERT_STORE_ADD_REPLACE_EXISTING, 0);
The CertGetCertificateChain MSDN documentation says
hAdditionalStore A handle to any additional store to search for supporting certificates and certificate trust lists (CTLs).
As far as I understand if I create a CTL with the root certificate and place it to the store, CertGetCertificateChain will trust it. So, I create the root certificate CTL entry in an allocated buffer and then copy it to std::vector ctlEntries
CertCreateCTLEntryFromCertificateContextProperties(rootCertContext, 0, nullptr, CTL_ENTRY_FROM_PROP_CHAIN_FLAG, nullptr, ctlEntry, &size);
Then I create the CTL itself.
const std::wstring ctlID(L"TrustedRoots");
// I do not know what OIDs to use here. I tried different options.
std::vector<LPSTR> usageList;
usageList.push_back(szOID_SORTED_CTL);
usageList.push_back(szOID_PKIX_KP_CLIENT_AUTH);
usageList.push_back(szOID_PKIX_KP_SERVER_AUTH);
CTL_INFO ctlInfo;
ZeroMemory(&ctlInfo, sizeof(ctlInfo));
ctlInfo.dwVersion = CTL_V1;
ctlInfo.SubjectUsage.cUsageIdentifier = static_cast<DWORD>(usageList.size());
ctlInfo.SubjectUsage.rgpszUsageIdentifier = usageList.data();
ctlInfo.ListIdentifier.cbData = static_cast<DWORD>((ctlID.size() + 1) * sizeof(wchar_t));
ctlInfo.ListIdentifier.pbData = static_cast<BYTE*>(static_cast<void*>(const_cast<wchar_t*>(ctlID.data())));
ctlInfo.SubjectAlgorithm.pszObjId = szOID_OIWSEC_sha1;
ctlInfo.cCTLEntry = static_cast<DWORD>(ctlEntries.size());
ctlInfo.rgCTLEntry = const_cast<PCTL_ENTRY>(ctlEntries.data());
// From MSDN:
// The message can be encoded without signers if the cbSize member of the structure is set to the
// size of the structure and all of the other members are set to zero.
CMSG_SIGNED_ENCODE_INFO encode;
ZeroMemory(&encode, sizeof(encode));
encode.cbSize = sizeof(encode);
DWORD size = 0, flags = CMSG_ENCODE_SORTED_CTL_FLAG | CMSG_ENCODE_HASHED_SUBJECT_IDENTIFIER_FLAG;
if (CryptMsgEncodeAndSignCTL(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, &ctlInfo, &encode, flags, nullptr, &size) == TRUE)
{
std::string data;
data.resize(size);
BYTE* p = static_cast<BYTE*>(static_cast<void*>(&data.front()));
if (CryptMsgEncodeAndSignCTL(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, &ctlInfo, &encode, flags, p, &size) == TRUE)
{
PCCTL_CONTEXT ctlContext = CertCreateCTLContext(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, p, size);
if (ctlContext)
{
if (CertAddCTLContextToStore(certStore, ctlContext, CERT_STORE_ADD_REPLACE_EXISTING, nullptr) == TRUE)
{
// success
}
}
}
}
All API calls above finish successfully but when I call CertGetCertificateChain, it still returns CERT_TRUST_IS_UNTRUSTED_ROOT in TrustStatus.dwErrorStatus.
Potential Workaround
If I get the CERT_TRUST_IS_UNTRUSTED_ROOT error, I just extract the CTL from the certificate store and check if the root from the result chain (returned by CertGetCertificateChain) is in the CTL. It works but is still not fully acceptable for me. I would like to rely on CertGetCertificateChain.
What is wrong with the solution? What specific Certificate Trust List OIDs must I use? Is any requirement (like specific extensions) for the root certificate to be trusted in this case?
p.s. The test certificates are created using this instruction https://gist.github.com/fntlnz/cf14feb5a46b2eda428e000157447309
UPD: 2020-01-31
CertModifyCertificatesToTrust did not help. It finishes successfully but the chain is still reported as having an untrusted root. Probably, the issue is in different area.
PCCERT_CONTEXT copiedCert = nullptr;
BOOL result = CertAddCertificateContextToStore(certStore,
cert, CERT_STORE_ADD_REPLACE_EXISTING, &copiedCert);
CertFreeCertificateContext(cert);
if (result)
{
// Save the certificate to create a CTL entry later
trustedRoots.push_back(copiedCert);
}
...
// Creating the CTL entries
...
std::vector<LPSTR> usageList;
usageList.push_back(szOID_CTL); // I really do not know what IDs I must use here
...
CTL_INFO ctlInfo;
ZeroMemory(&ctlInfo, sizeof(ctlInfo));
ctlInfo.dwVersion = CTL_V1;
ctlInfo.SubjectUsage.cUsageIdentifier = static_cast<DWORD>(usageList.size());
ctlInfo.SubjectUsage.rgpszUsageIdentifier = usageList.data();
...
// Should I use any of the flags?
DWORD size = 0, flags = 0; /*CMSG_ENCODE_SORTED_CTL_FLAG | CMSG_ENCODE_HASHED_SUBJECT_IDENTIFIER_FLAG;*/
if (CryptMsgEncodeAndSignCTL(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, &ctlInfo, &encode, flags, nullptr, &size) == TRUE)
...
if (CertAddCTLContextToStore(certStore, ctlContext, CERT_STORE_ADD_REPLACE_EXISTING, nullptr) == TRUE)
{
// Check that the CTL is in the store and the root certificate is in the CTL
CTL_FIND_USAGE_PARA usagePara;
ZeroMemory(&usagePara, sizeof(usagePara));
usagePara.cbSize = sizeof(usagePara);
usagePara.SubjectUsage.cUsageIdentifier = 0;
usagePara.ListIdentifier.cbData = static_cast<DWORD>((ctlID.size() + 1) * sizeof(wchar_t));
usagePara.ListIdentifier.pbData = static_cast<BYTE*>(static_cast<void*>(const_cast<wchar_t*>(ctlID.data())));
PCCTL_CONTEXT foundCTLContext = CertFindCTLInStore(certStore,
X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, 0, CTL_FIND_USAGE,
static_cast<void*>(&usagePara), nullptr);
if (foundCTLContext != nullptr)
{
PCTL_ENTRY ctlEntry = CertFindSubjectInCTL(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
CTL_CERT_SUBJECT_TYPE, const_cast<void*>(*trustedRoots.begin()), foundCTLContext, 0);
if (ctlEntry != nullptr)
{
// It means the root certificate has been correctly added to the CTL and the CTL is in the store.
std::cout << "Found the certificate in the CTL" << std::endl;
}
}
// Make the certificate trusted via CertModifyCertificatesToTrust
HMODULE module = LoadLibrary(L"CryptDlg.dll");
if (module)
{
CertModifyCertificatesToTrustPfn pfn =
(CertModifyCertificatesToTrustPfn)GetProcAddress(hModule, "CertModifyCertificatesToTrust");
if (pfn != nullptr)
{
CTL_MODIFY_REQUEST req;
// Only one certificate is in the trustedRoots store curretly
req.pccert = static_cast<PCCERT_CONTEXT>(*trustedRoots.begin());
req.dwOperation = CTL_MODIFY_REQUEST_ADD_TRUSTED;
req.dwError = 0;
HRESULT hr = pfn(1, &req, szOID_CTL, NULL, certStore, nullptr);
if (hr == S_OK)
{
// Success
std::cout << "Modified" << std::endl;
}
}
}
}
The CertGetCertificateChain function builds a certificate chain context starting from an end certificate and going back, if possible, to a trusted root certificate. A handle of the chain engine (namespace and cache) to be used. If hChainEngine is NULL, the default chain engine, HCCE_CURRENT_USER, is used.
Microsoft uses CTL to distribute a list of trusted root certificates which are members of Microsoft Root Program, explicitly distrusted (compromised) certificates: Sysadmins.PKI.dll library (part of PSPKI module) provides a set of APIs to generate arbitrary CTL. X509CertificateTrustListBuilder is a base class to build CTL.
Depending on store where CTL is installed, certificates in CTL are trusted or distrusted by operating system or application. As said, CTL is often used to logically group multiple certificates for specified purposes and trust/distrust for specified duration. Instead of managing multiple separate certificates, you manage only single container.
In addition to certificates and certificate revocation lists (CRL), the CryptoAPI certificate store supports the certificate trust list (CTL). A CTL is a predefined list of items signed by a trusted entity. A CTL is a list of hashes of certificates or a list of file names.
You can try to use the following api: CertModifyCertificatesToTrust
And note that
This function has no associated import library. You must use the LoadLibrary and GetProcAddress functions to dynamically link to CryptDlg.dll.
Set the CTL_MODIFY_REQUEST
.dwOperation
to the flagCTL_MODIFY_REQUEST_ADD_TRUSTED
to add the certificate to the CTL. The certificate is explicitly trusted.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With