sslclickoncecode-signingcode-signing-certificate

"Unknown Publisher" on non-domain machine using AD-created code signing certificate


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.

Success message

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 "Untrusted Publisher" when I double-click either the .application file or setup.exe.

Error message

Here's my ClickOnce setup, pretty straightforward:

Signing tab Publish tab

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?

--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."


Solution

  • 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 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 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:

    1. Run certsrv.msc on the CA server desktop
    2. Right-click the CA name and choose Properties
    3. Select the Extensions tab
    4. Select the CDP extension
    5. Click Add to add your HTTP URL (HTTPS won't work)
    6. Enter the URL in this form, including the angle brackets and token: http://subdomain.domain.com/folder/list<DeltaCRLAllowed>.crl
    7. Click OK
    8. With your new CDP selected, select these two checkboxes:
      • Include in CRLs. Clients use this to find Delta CRL locations.
      • Include in the CDP extension of issued certificates.
    9. Click OK again
    10. Accept the prompt to restart the AD service
    11. Publish your new CRLs by right-clicking Revoked Certificates in certsrv.msc and choosing Publish. Select New and click OK.

    Now use certmgr.msc to request a new code signing certificate, just the way you did before (you do remember, right?). 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.

    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.