I've used my Active Directory CA to generate an AuthentiCode-compatible code signing certificate, and I'm using it to sign my ClickOnce application and manifest. Everything works fine on my domain-joined machines; the ClickOnce installation dialog reports the cert's Common Name as the application's publisher and I get a green checkmark.
Things are different on non-joined machines, however. It's not going so well. I've installed the Root CA in the Trusted CAs store and the code signing cert's public key in both the Personal and Trusted Publisher's stores. But I still get "Unknown Publisher" when I double-click either the .application
file or setup.exe
.
Here's my ClickOnce setup, pretty straightforward:
I've verified that my signer has the Code Signing (1.3.6.1.5.5.7.3.3)
requirement in its Extended Key Usage property, and that I'm using the Microsoft Enhanced RSA and AES Cryptographic Provider in my AD certificate template.
ProcMon collects an absolutely mind-numbing amount of data when Setup.exe
runs; I wouldn't know where to even begin looking in all of that.
I'm at a loss as to what else to try. I'm finding plenty of Q&As (too many to efficiently list here, frankly) for scenarios that involve big-name cert providers (Sectigo, etc.) on both domain-joined and non-joined machines, as well as plenty of Q&As for scenarios that involve AD certs on joined machines, but I'm coming up empty for my particular scenario—an AD cert on a non-joined machine.
According to this guidance, if I'm reading it correctly (and I think I am), the code signing cert must also be imported into the Trusted CAs. Which is odd... I've never heard of a non-CA cert having to be in the Trusted CAs store, but shucks—I won't complain. Except that I tried it and it still doesn't work.
Oh... it's worth mentioning... through all of this I've taken care to ensure I'm importing all certs at both the Current User and Local Machine levels, just in case.
Is this by design? Is ClickOnce intended to disallow AD code signing certs on non-domain clients?
How do I get ClickOnce to trust my code signing certificate on a non-domain machine?
--EDIT--
In a fit of last-ditch futility, I imported my Root CA into the Intermediate CAs store as well, both Current User and Local Machine. I got the same result: "Unknown Publisher."
This whole thing didn't sound right to me, so I went and did some digging.
It turns out that the Unknown Publisher result has nothing directly to do with whether or not the machine is a member of a domain, and everything to do with whether the CA for the code signing certificate publishes a publicly-accessible Certificate Revocation List (CRL).
The default configuration when the Active Directory Certificate Services role is added includes only one network-based CRL Distribution Point (CDP), and that CDP is behind an LDAP URL. And of course that LDAP URL can't be accessed from non-domain machines.
So the root cause isn't specifically joined/non-joined; that's only an artifact. But the end result is the same, of course, so that's what was throwing me off.
And yes, there is a solution. All we have to do is find a public location to host our CRLs (a CDP) and add that location to our code signing certificate. I say CRLs (plural) because AD publishes two versions: a Base CRL (all revocations) and a Delta CRL (new revocations since the last base publish). The files are named according to the CDP settings, for which the default is:
C:\Windows\System32\CertSrv\CertEnroll\<CaName><CRLNameSuffix><DeltaCRLAllowed>.crl
Those tokens expand to these values:
<CaName> The name of the CA
<CRLNameSuffix> Appends a suffix to distinguish the CRL file name
<DeltaCRLAllowed> Substitutes the Delta CRL file name suffix for the CRL file name suffix, if appropriate
If your CA has been running for a bit with the setup defaults configured, you'll notice that one of the filenames has a +
sign at the end. This indicates a Delta CRL.
To get to all of this:
http://subdomain.domain.com/folder/list<DeltaCRLAllowed>.crl
Now use certmgr.msc (or certlm.msc for all users) to request a new code signing certificate, just the way you did before (you do remember, right?). Once it's issued, check its properties, and on the Details tab in the CRL Distribution Points field you should see the CDP URL that you added above. Sign your app and manifest with this new certificate. (For Kicks&Grins™, double-click your Base CRL file and check the value of its Freshest CRL field—you'll find the URL for your Delta CRL.)
OK, that's the first part. Now we need to set up hosting for the actual CDP. The best place I found to do that is Block Blobs on Azure Storage. IIS might work, if you're hosting locally and you're able to Allow Double Escaping—discussed here and here—for the +
sign in the URL. But if you're hosting on an Azure App Service, like I am, that's not an option (maybe the PowerShell command would work, but I didn't try it).
So create a Blob container and upload your two CRL files. The container name becomes the folder in your URL.
Next, you may wish to create a Custom Domain for your Azure Storage Account, which is what I did to get the URL syntax above. The documentation for that task can be found here.
Now, once all that's in place, it's time to test. Drop into a command prompt on the non-joined machine and enter this command:
certutil -url http://subdomain.domain.com/folder/list.crl
(Substitute your preferred values, of course.)
The URL Retrieval Tool will run. Click Retrieve. You should get two successes and one failure, with the one failure being for the LDAP URL. The two successes will be for your Base and Delta CRLs.
You're all set! As long as your Root CA is installed on this machine, either in Local Machine or Current User, you should get a Trusted Publisher result, even when Check for publisher's certifcate revocation is selected in Internet Options.
If you want to update your CRLs regularly, and I'm sure you will, it'll be an easy task to build a Windows service that uses the Azure SDK to upload the files on a regular basis.
Hopefully this blog post can help someone who ran into the same problem.