CertGetCertificateChain with a supporting memory store and Certificate Trust List
Asked Answered
U

1

6

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;
            }
       }
   }                                    

}

Undercoat answered 30/1, 2020 at 16:48 Comment(1)
Check out schannel_verify.c for a working impl. They call CertGetCertificateChain against all (PEM encoded) root certificates in curl-ca-bundle.crt which at some point get added to a HCERTSTORE (in add_certs_to_store function) that is set to hExclusiveRoot member of CERT_CHAIN_ENGINE_CONFIG.Genius
C
6

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.

Continuate answered 31/1, 2020 at 11:25 Comment(3)
Drake, thank you for your answer. Although looks like the modification of the CTL/store is successful, the chain is reported as untrusted. I think that maybe even the initial CTL creation algorithm works correctly but something wrong with flags, etcUndercoat
I have submit an issue to the relevant engineer, I will update here ASAP if there is any response, please wait patiently.Continuate
@Undercoat UPDATE: "The behavior of function CertGetCertificateChain seems expected, and we suggest you to use the workaround."Continuate

© 2022 - 2024 — McMap. All rights reserved.