1

I'm needing to use C++ to get a certificate from the local machine store because Unity & Mono don't support the local machine store correctly.

To that end I've implemented the following, but my knowledge of pointers, references, address of and C++ function lifetime is stopping me from getting a valid pointer back. I know there's likely multiple sins in the below, but first, please help me get 'out' a pointer to the found certificate that isn't nuked by memory management.

X509CertificateLookup.h

#define DLLExport __declspec(dllexport)

extern "C"
{
    DLLExport bool GetMachineCertByThumb(void**& pCertContext, const char* thumbprint, const char* storeName);
    DLLExport void FreeCertificateContext(const void* certPtr);
}

#endif

X509CertificateLookup.cpp

bool GetMachineCertByThumb(void**& pCertContext, const char* thumbprint, const char* storeName) {
    HCERTSTORE hCertStore = NULL;

    if ((hCertStore = CertOpenStore(
        CERT_STORE_PROV_SYSTEM_W,        // The store provider type
        0,                               // The encoding type is not needed
        NULL,                            // Use the default HCRYPTPROV
        CERT_SYSTEM_STORE_LOCAL_MACHINE | CERT_STORE_READONLY_FLAG, // Set the store location in a registry location
        storeName                        // The store name as a Unicode string (L"MY") 
    )) != nullptr) {
        PCCERT_CONTEXT pSearchCertContext = NULL;

        if ((pSearchCertContext = CertFindCertificateInStore(
            hCertStore,
            X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
            0,
            CERT_FIND_HASH_STR,
            thumbprint,
            pSearchCertContext)) != nullptr) {

            CertCloseStore(hCertStore, CERT_CLOSE_STORE_CHECK_FLAG);

            //A pointer to a buffer that contains the encoded certificate
            pCertContext = &pSearchCertContext; //<- This is where my issues are!
            return true;
        }
    }

    if (hCertStore) {
        CertCloseStore(hCertStore, CERT_CLOSE_STORE_CHECK_FLAG);
    }

    pCertContext = nullptr;
    return false;
}

void FreeCertificateContext(const void* certPtr) {
    if (certPtr == nullptr) {
        CertFreeCertificateContext(static_cast<PCCERT_CONTEXT>(certPtr));
    }
}

X509CertLookupWorkaround.cs

class X509CertificateLookup {
    [DllImport(nameof(X509CertificateLookup), CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi, ExactSpelling = true)]
        [return: MarshalAs(UnmanagedType.I1)]
        public static extern bool GetMachineCertByThumb(
            ref IntPtr pCertContext,
            [MarshalAs(UnmanagedType.LPUTF8Str)]
            string thumbprint,
            [MarshalAs(UnmanagedType.LPUTF8Str)]
            string storeName);

    [DllImport(nameof(X509CertificateLookup), CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
    public static extern void FreeCertificateContext([In] IntPtr certPtr);
}

class X509CertLookupWorkaround{

    public static X509Certificate2 GetMachineCertByThumb_CPP(string thumbprint, string storeName) {

        IntPtr certPtr = IntPtr.Zero;
        
        if(!X509CertificateLookup.GetMachineCertByThumb(ref certPtr, thumbprint, storeName) || certPtr == IntPtr.Zero) {
            UnityEngine.Debug.Log("Failure, Certificate not found!");
            return null;
        }else{
            UnityEngine.Debug.Log("Success, Certificate found!");
            return new X509Certificate2(certPtr);
        }
    }
    
    public static void ReleaseCertificate(IntPtr certPtr){
        X509CertificateLookup.FreeCertificateContext(certPtr)
    }
}


Reahreic
  • 596
  • 2
  • 7
  • 26
  • `void**&` is a very ugly type to marshal (and honestly, I would even need to wrap my head around that in C++). Can't you use something simpler, such as only `void**`? – PMF Jul 14 '23 at 19:22
  • `void**& pCertContext` -- Do not use reference parameters as arguments to an exported DLL function. A reference can only be used by C++ applications, and even then, it must be used by the same compiler and compiler version. BTW, reference parameters are only for C++, but you also tagged `C`, and reference arguments do not exist in C. – PaulMcKenzie Jul 14 '23 at 19:23
  • `pCertContext = &pSearchCertContext` -- You are returning the address of a local variable. That variable goes up in a puff of smoke as soon as that function returns. So it isn't surprising things are not working. – PaulMcKenzie Jul 14 '23 at 19:28
  • Similar to my first comment, `bool` in C++ is only known to C++. You should be passing and returning types that Windows knows about -- `LONG`, `DWORD`, `BOOL`, `LPCSTR`, etc. – PaulMcKenzie Jul 14 '23 at 19:35
  • Do you have visual studio? If so have a look at writing a wrapper in C++/cli you get much more control that way. E.g. [C++/cli in 10 minutes](https://www.codeproject.com/Articles/19354/Quick-C-CLI-Learn-C-CLI-in-less-than-10-minutes). You can then define a .Net datastructure in C++/cli (e.g. a byte buffer) and copy the C++ data into that. – Pepijn Kramer Jul 14 '23 at 19:48
  • If changing the interface is possible, probably this would be a better interface: `DLLExport PCCERT_CONTEXT GetMachineCertByThumb(const char* thumbprint, const char* storeName);` -- Then you're not messing around with void pointers as parameters, and C# just needs the pinvoke for it to return an `IntPtr` value. – PaulMcKenzie Jul 14 '23 at 20:58
  • Change the parameter type to PCCERT_CONTEXT*, change the assignment to *pCertContext = pSearchCertContext; – Hans Passant Jul 14 '23 at 22:54
  • Why don't you just do all of the PInvoke calls from C# and forgo C++ completely? – Charlieface Jul 16 '23 at 05:54
  • Yeah, I don't honestly understand the `**&` myself, to begin with I used `void*` but after multiple failings and far too much googling the `**&` was just the latest incarnation by the time I called it quits on Friday. – Reahreic Jul 17 '23 at 11:08
  • I only tagged C as I thought that was applicable to the `Extern "C"` portion. All 3 files are under my control, so I can change them however needed. I did originally try returning `PCCERT_CONTEXT` directly as well as using `BOOL`, but couldn't get the `DLLExport` to accept it in the function signature for `PCCERT_CONTEXT`, and was getting mismatches between how C++ BOOL and C# bool's are interpreted in the return. (another stack overflow question I came across indicated to rather use `bool`) – Reahreic Jul 17 '23 at 11:15
  • Why people told you to beware of `bool` -- There are four different boolean types in use for Windows DLL return values: "C++ `bool`", "Windows `BOOL` typedef, actually int-sized", "Windows `BOOLEAN` typedef, actually byte-sized", and `VARIANT_BOOL`. https://devblogs.microsoft.com/oldnewthing/20041222-00/?p=36923 The p/invoke declaration on the C# side is different for each of these four cases. – Ben Voigt Jul 17 '23 at 19:31
  • The code in the question handling `bool` is OK because it uses `[return: MarshalAs(UnmanagedType.I1)]`. There is an actual problem on Windows machines where the callee using `bool` may only set the low 8 bits of the return value register, leaving the upper 24 (or 56) bits unchanged, and then the caller may look at 32 bits. The attribute used in the question prevents the C# caller from looking at 32 bits, so it should not actually matter if the C code is leaving some unchanged or zeroing all the high bits. – Ben Voigt Jul 17 '23 at 19:31

3 Answers3

1

For starters, it should be

bool GetMachineCertByThumb(
   void*& pCertContext,    // A reference to a `void*` variable to which to write.
   const char* thumbprint,
   const char* storeName
) {
   ...
   pCertContext = pSearchCertContext;

   ...
}

You want to "return" the pointer returned by CertFindCertificateInStore, not a pointer to the local variable that contains this pointer.


Then, there's a potential issue in the use of a reference type in an extern "C" section. C doesn't have reference types, so let's avoid it.

bool GetMachineCertByThumb(
   void** ppCertContext,    // A pointer to a `void*` variable to which to write.
   const char* thumbprint,
   const char* storeName
) {
   ...
   *ppCertContext = pSearchCertContext;
   ...
}

But why return the value via a parameter? It would be a lot simpler to return it as a returned value.

void *GetMachineCertByThumb( const char* thumbprint, const char* storeName ) {
   ...
   return pSearchCertContext;
}

I don't know if there are other issues.

ikegami
  • 367,544
  • 15
  • 269
  • 518
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/254506/discussion-between-paulmckenzie-and-ikegami). – PaulMcKenzie Jul 14 '23 at 20:31
  • Re "*I'd previously tried `void**` to no avail*", I can't comment on code you didn't provide. The fact that your code using `void**` didn't work has no bearing on this. – ikegami Jul 17 '23 at 18:53
  • **Please excuse the mess of irrelevant comments @Paul McKenzie made until the moderators clean it up** – ikegami Jul 17 '23 at 19:44
1

I would try to avoid the void** parameter as much as possible.

Since the C++ code gets a pointer to the buffer (and from the looks of it, the buffer is not local), this may be a better interface (or at least this is how I would look to implement it):

PCCERT_CONTEXT GetMachineCertByThumb(LPCSTR thumbprint, LPCSTR storeName) 
{
    HCERTSTORE hCertStore = NULL;

    if ((hCertStore = CertOpenStore(
        CERT_STORE_PROV_SYSTEM_W,        // The store provider type
        0,                               // The encoding type is not needed
        NULL,                            // Use the default HCRYPTPROV
        CERT_SYSTEM_STORE_LOCAL_MACHINE | CERT_STORE_READONLY_FLAG, // Set the store location in a registry location
        storeName                        // The store name as a Unicode string (L"MY") 
    )) != nullptr) {
        PCCERT_CONTEXT pSearchCertContext = NULL;

        if ((pSearchCertContext = CertFindCertificateInStore(
            hCertStore,
            X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
            0,
            CERT_FIND_HASH_STR,
            thumbprint,
            pSearchCertContext)) != nullptr) {

            CertCloseStore(hCertStore, CERT_CLOSE_STORE_CHECK_FLAG);

            //A pointer to a buffer that contains the encoded certificate
            return pSearchCertContext; 
        }
        return NULL;
    }

    if (hCertStore) {
        CertCloseStore(hCertStore, CERT_CLOSE_STORE_CHECK_FLAG);
    }

    return NULL;
}

The above interface avoids the void** parameter, as the function returns the pointer to the actual buffer (if found), or NULL (if not found).

On the C# side of the fence for the pinvoke:

[DllImport(nameof(X509CertificateLookup), CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi, ExactSpelling = true)]
public static extern IntPtr GetMachineCertByThumb([MarshalAs(UnmanagedType.LPUTF8Str)] string thumbprint, [MarshalAs(UnmanagedType.LPUTF8Str)] string storeName);

Then in the C# calling code itself, you will be testing for the IntZero, and not a return value of true or false:

class X509CertLookupWorkaround
{
    public static X509Certificate2 GetMachineCertByThumb_CPP(string thumbprint, string storeName) 
    {
        IntPtr certPtr = X509CertificateLookup.GetMachineCertByThumb(thumbprint, storeName);
        if (certPtr == IntPtr.Zero) 
        {
            UnityEngine.Debug.Log("Failure, Certificate not found!");
            return null;
        }
        else
        {
            UnityEngine.Debug.Log("Success, Certificate found!");
            return new X509Certificate2(certPtr);
        }
    }
 //...
}
PaulMcKenzie
  • 34,698
  • 4
  • 24
  • 45
  • Any time I use `PCCERT_CONTEXT` as a return type in the `extern "c"` portion of the header file I'm unable to get VS to compile as it clashes with the method declaration inside my .cpp file with `cannot overload functions distinguished by return type alone` As far as I know I need the DLLExport portion for C# to pick it up. Is the overloaf in the .cpp file actually needed, I thought it was not good practice to implement an entire method inside the header file? – Reahreic Jul 17 '23 at 19:02
  • I don't quite understand the issue. Is the `PCCERT_CONTEXT GetMachineCertByThumb(LPCSTR thumbprint, LPCSTR storeName)` the exported function? If so, why do you have another function with the same name, but different return type? My answer was done so that it *replaces* your existing function, not add on top of it. The `extern "C"` should work the same way as it always does with exported functions. – PaulMcKenzie Jul 17 '23 at 19:08
  • That's just it, regardless of what I do I can't get` PCCERT_CONTEXT` to be allowable by VS2022 in the `extern "C"` declaration. I even created a new exported method `DLLExport PCCERT_CONTEXT TestingReturnContextOnly();` and still get `E0311 cannot overload functions by return type alone` even when there is no other TestingReturnContextOnly() method in in the entire solution. – Reahreic Jul 17 '23 at 19:23
  • @Reahreic: You have at least a function definition (with body) in addition to that declaration. If those two don't match, the compiler assumes you are trying to overload. Make them match. – Ben Voigt Jul 17 '23 at 19:27
  • @Reahreic Well, if you disregard the possible issues with godbolt and MSVC, [this code](https://godbolt.org/z/zr63fox74) compiles with no errors. – PaulMcKenzie Jul 17 '23 at 19:31
  • @Reahreic Re "*as it clashes with the method declaration inside my .cpp file*", Sounds like you forgot to adjust the definition in the CPP file to match the declaration. You need `PCCERT_CONTEXT GetMachineCertByThumb(LPCSTR thumbprint, LPCSTR storeName)` in both places. (Alternatively, you could create a wrapper with a different name.) – ikegami Jul 17 '23 at 19:40
  • @PaulMcKenzie I don't know what yo tell you, If I paste the godbolt code into the IDE it gives me the message, If I paste what I typed into the IDE into godbolt, there's no error. This https://godbolt.org/z/dc6ss84T7 works exactly as you describe, but not when I retype it into the IDE. My project is set to use C++20 and C17 with Platform: VS 2022 (v143) and Windows SDK: 10.0.22621.0 – Reahreic Jul 17 '23 at 20:06
  • Are you using precompiled headers? That's the only thing I can think of that may cause issues like this, that is, if you really don't have those other function declarations floating around somewhere, and the compiler is picking them up. – PaulMcKenzie Jul 17 '23 at 20:09
  • VS is set to not use precompiled headers, and there's no stdafx.h file in the solution. I do have `#define WIN32_LEAN_AND_MEAN` in my .h file and aside from a few includes nothing that stands out in the .cpp file. – Reahreic Jul 18 '23 at 11:14
  • Well, there is something really wrong with your environment that you should investigate. Who knows what other issues that you are not aware of that are hidden away. I just took the same code in godbolt, and was able to compile it locally, VS 2022, etc. – PaulMcKenzie Jul 18 '23 at 16:42
1

You are probably better off just doing the whole thing in C# and PInvoke, rather than messing around trying to marshal to/from C++ as well.

Create SafeHandle classes for the store and context handles, and put them in using to dispose them properly.

class X509CertificateLookup
{
    const int CERT_CLOSE_STORE_CHECK_FLAG = 0x2;
    const int CERT_SYSTEM_STORE_LOCAL_MACHINE = 0x20000;
    const int CERT_STORE_READONLY_FLAG = 0x8000;
    const int X509_ASN_ENCODING = 0x1;
    const int PKCS_7_ASN_ENCODING = 0x10000;
    const int CERT_FIND_HASH_STR = 0x140000;
    const int CRYPT_E_NOT_FOUND = unchecked((int)0x80092004);

    [DllImport("Crypt32.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
    static extern SafeCertStoreHandle CertOpenStore(
      [MarshalAs(UnmanagedType.LPStr)] string lpszStoreProvider,
      int dwEncodingType,
      IntPtr hCryptProv,
      int dwFlags,
      [MarshalAs(UnmanagedType.LPWStr)] string pvPara
    );

    [DllImport("Crypt32.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
    static extern SafeCertContextHandle CertFindCertificateInStore(
      SafeCertStoreHandle hCertStore,
      int dwCertEncodingType,
      int dwFindFlags,
      int dwFindType,
      [MarshalAs(UnmanagedType.LPWStr)] string pvFindPara,
      SafeCertContextHandle pPrevCertContext
    );

    class SafeCertContextHandle : SafeHandleZeroOrMinusOneIsInvalid
    {
        public SafeCertContextHandle(IntPtr handle) : base(true)
        {
            SetHandle(handle);
        }

        [DllImport("Crypt32.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
        private static extern bool CertFreeCertificateContext(IntPtr pCertContext);

        override protected bool ReleaseHandle()
        {
            if (!IsInvalid)
                CertFreeCertificateContext(handle);
            return true;
        }
    }

    class SafeCertStoreHandle : SafeHandleZeroOrMinusOneIsInvalid
    {
        public SafeCertStoreHandle(IntPtr handle) : base(true)
        {
            SetHandle(handle);
        }

        [DllImport("Crypt32.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
        static extern bool CertCloseStore(IntPtr hCertStore, int dwFlags);

        override protected bool ReleaseHandle()
        {
            if (!IsInvalid)
                CertCloseStore(handle, CERT_CLOSE_STORE_CHECK_FLAG);
            return true;
        }
    }
    public static X509Certificate2 GetMachineCertByThumb(string thumbprint, string storeName)
    {
        using var hCertStore = CertOpenStore(
            "CERT_STORE_PROV_SYSTEM_W",      // The store provider type
            0,                               // The encoding type is not needed
            IntPtr.Zero,                     // Use the default HCRYPTPROV
            CERT_SYSTEM_STORE_LOCAL_MACHINE | CERT_STORE_READONLY_FLAG, // Set the store location in a registry location
            storeName
        );

        if (hCertStore.IsInvalid)
            throw new Win32Exception(Marshal.GetLastWin32Error());

        using var pSearchCertContext = CertFindCertificateInStore(
            hCertStore,
            X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
            0,
            CERT_FIND_HASH_STR,
            thumbprint,
            null);

        if (pSearchCertContext.IsInvalid)
        {
            var errorCode = Marshal.GetLastWin32Error();
            if (errorCode == CRYPT_E_NOT_FOUND)
                return null;
            throw new Win32Exception(errorCode);
        }
        return new X509Certificate2(pSearchCertContext.DangerousGetHandle());
    }
}
Charlieface
  • 52,284
  • 6
  • 19
  • 43