Some time ago I wrote a script to create an arbitrary self signed certificate. Then I made a DevOps pipeline task out of it. Now, I'm wondering about the same logic, running interactively.
Naturally, in an interactive scenario, one would want a GUI for providing all the certificate properties. Putting one together is entirely possible, even in PowerShell - there is, among others, a nice module called WinGUI that wraps .NET WinForms. But what about the Windows' own certificate enrollment UI? You get to see this UI when you open the Windows' certificate manager, and click "Create Custom Request" under "Advanced". The Properties window on the fourth page of the wizard has all the controls we need. There is a friendly UI for distinguished name composition, certificate extension editing logic with both standard extensions and custom ones, etc. I'm talking about this enrollment wizard:
There is already a built-in certificate viewer in Win32 - is the enrollment UI also programmable?
It kind of is. The certificate management MMC snap-in, the one that implements the "Manage computer certificates"/"Manage user certificates" administrative tools, calls into undocumented functions in CertEnroll.dll, which in turn call into the undocumented library CertEnrollUI.dll. Thanks to the magic of the Symbol Server, the interface of that one can be recovered. There is one exported function:
HRESULT CreateUIObject(ICertificateEnrollmentUI**);
which returns a pointer to a class full of virtual functions. It looks a bit like a COM object, but it's not - there is no IUnknown there. To avoid large code snippets in the blog, I'll refer you to the gist where I've documented the API. The parameters of its methods are mostly documented CertEnroll and ATL data structures, with some exceptions. The CertEnrollUI.h, as written down in the gist, does not depend on ATL.
The way this API is supposed to be used is quite unusual. The consumer is expected to get the UI object, call Run() on it, which blocks until the wizard finishes, and then call other methods of the UI object on a background thread. Those methods of the UI object correspond to the pages of the wizard, meaning that calling one brings up the respective page of the wizard, and the method call blocks the background thread until the user clicks Next or Cancel. The UserAction out parameter tells you which was it.
Once Run() returns, delete the UI object by calling its Delete() method.
To demonstrate all of this, here is a C++ gist which calls the CertEnrollUI API. It pops up the certificate enrollment dialog (with no policies), lets one fill out the certificate properties, and retrieves those in a ready to consume form.
All the certificate properties that a piece of logic for creating a self signed certificate would need are on the fourth page - "Certificate Information", under the Properties button. In theory, it might be possible to skip the first three pages and go directly to the fourth one - but if you do, the Next button won't be enabled. I guess the wizard stores inputs (either from the caller, or from the user) from the first three pages, and misbehaves if those are missing.
The parameters of the page methods correspond to the controls on the respective page - both on the input and on the output. The certificate details page is brought forth by calling ShowTemplateSelectionPage(), and you are supposed to pass a a collection of IX509CertificateEnrollment objects to it. The collection interface is ostensibly read/write, but the UI only reads it, and only uses the first two functions - get_ItemByIndex and get_Count. It's OK to return E_NOTIMPL for the rest of the collection methods.
The collection contains enrollment objects, which wrap a certificate request. The certificate request inside the enrollment object is supposed to be initialized, and certain fields should be already set (otherwise the UI displays an error message). I didn't interrogate this logic too hard - in the gist, the request properties that I set before displaying the page correspond to what CertEnroll itself does. Most of the request properties are set using public, documented CertEnroll interfaces, but one is undocumented - the IX509EnrollmentInternal::PutQueryStatus(). I've tried - this one is required, the UI marks the request line as failed if it's omitted. I didn't feel like spelling out the whole private interface, and only provided just enough to initialize this one property.
Most of my PKI writeups tend to feature code snippets in PowerShell. Not because I love PowerShell excessively, but because when it comes to presenting your code to strangers on the Internet, I feel that a scripting, shell adjacent language is an easier sell - it's not as threatening as C++. Also, the need to build is also a potential source of friction. But for this one, PowerShell is hardly a natural fit. To wit, this API expects one to:
- Run a background thread
- Consume an undocumented, private COM interface
- Implement an undocumented, private COM interface
No comments:
Post a Comment