How to Authenticode sign ClickOnce deployment with an EV SHA2 cert and avoid "Unknown Publisher"
Asked Answered
D

5

8

When signing my ClickOnce deployment via Visual Studio's project "Signing" settings page I specified our SHA2 (SHA256) EV Authenticode certificate and publish.

enter image description here

After publishing and attempting to run the bootstrapper (setup.exe) I'm presented with the "Unknown Publisher" in the ClickOnce dialog.

enter image description here

The EV certificate in question is valid and running on an eToken hardware token with SafeNet client tools to communicating with the token. Signing regular PE files (exe and dll) with signtool always produces perfectly valid assemblies and the publisher is known. This is only an issue with ClickOnce deployments. In addition, the individual files of the ClickOnce deployment look perfectly valid because the digital signatures tab of the file properties dialog is listed correctly for the bootstrapper (setup.exe) and the assembly files suffixed with ".deploy".

enter image description here

Also, the ".application" and ".manifest" files are appropriately mutated (probably via mage by Visual Studio) to contain the <publisherIdentity> element along with the algorithm set correctly.

enter image description here

The signing machine is running Win10 and I've tried every permutation I could imagine:

  • With and without a timestamp
  • With and without strong name signing
  • With and without online publishing
  • With and without https online publishing
  • With and without specific "Update location" via Publish page
  • With and without "Publisher name" set via Description in Publish page
  • With every combination of Manifest options:
    • Exclude deployment provider URL
    • Block application from being activated via a URL
    • Use application manifest for trust information
  • Multiple machines on various versions of Windows
  • Manual manifest signing and assembly signing via mage and signtool (yes mageui as well)
  • Ensure the cert is not revoked with certificate provider

There appears to be someone else experiencing this.

Diuretic answered 16/9, 2016 at 18:59 Comment(1)
VisualStudio does not support ClickOnce signing using hardware tokens, please use Mage.eseBarbicel
D
4

The reason this occurs is due to a couple of factors:

  1. ClickOnce displays "Unknown Publisher" when using a SHA2 Authenticode certificate.
  2. On January 1st 2016 Windows deprecated SHA1 for Authenticode signing /code signing. Windows SmartScreen technology thus displays "Unknown Publisher" when using a SHA1 Authenticode certificate.

This is in effect a catch-22, you need SHA1 for ClickOnce publisher verification and SHA2 for SmartScreen. Nice.

Work with your certificate provider (hopefully a true CA) to get you a SHA1 and SHA2 certificate. The folks at DigiCert were great. You must work with your CA in most cases because even if you already have your own SHA2 cert and you work with them to also get a SHA1 cert (or vica-versa), it will likely auto-revoke any existing certificates you have with them. In the case of DigiCert they were able to prevent the automatic revocation when I explained what I wanted to try (dual signing).

After you've installed those on your EV token, configure Visual Studio to sign your ClickOnce manifests with your SHA1 certificate. Ideally you'll also supply a Timestamp server in that same dialog for the eventual expiration of your certificate.

enter image description here

After publishing your ClickOnce deployment locally and before distributing, dual sign your ClickOnce bootstrapper (setup.exe) by appending your SHA2 certificate.

signtool.exe sign /tr http://timestamp.digicert.com /td sha256 /fd sha256 /as /sha1 YourCertThumbprintHash "X:\Deployment\ClickOnceCert\setup.exe"

Note, one way to find your cert thumbprint is via the Certificates MMC snap-in. And yes, thumbprints are supposed to be SHA1 for SHA2 certs.

enter image description here

Now, the bootstapper shows both of your certificates in the Digital Signatures tab of the file properties dialog.

enter image description here

When you run the setup.exe from the location specified as your "Installation Folder URL" of your Publish page in Visual Studio, you should see the publisher as trusted. It's important to understand the Installation Folder because if you were to run the app from another location you should expect that not to be trusted because the bootstrapper will make calls to the known Installation Folder to retrieve Application Files.

enter image description here

Diuretic answered 16/9, 2016 at 19:57 Comment(3)
I tried to follow these directions. I contacted COMODO, who issued my SHA2 EV certificate about a month ago. They said, "We do not issue SHA1 certificates any longer." It was clear I was chatting with a functionary who had no ability to escalate the issue. I have no idea what to do next.Langsyne
@RobPerkins a bit old, but did you make any progress on this? I'm in the same situation with a GlobalSign EV certificate on a USB token...Vaginismus
Nothing. I would have to go through the expensive process of using a cert provider who will produce both kinds of certs. Not worth it right now.Langsyne
N
3

It seems that since Visual Studio 15.7.5 (or maybe previous version, I didn't check them) both setup.exe and application binary file are valid for ClickOnce when signed with SHA2 EV code signing certificate (no need to ask your certificate provider for SHA-1). I'm using Windows 10 (10.0.16299.492), we checked it also on Windows 8, both work fine. I can't tell if it's an effect of updated version of Visual Studio or SmartScreen. I failed to publish a signed ClickOnce application a year ago, now everything works fine.

Main application project signing properties: enter image description here

"Select from store" dialog:

enter image description here

Published ClickOnce setup.exe properties

enter image description here

Published ClickOnce application *.exe.deploy file properties

enter image description here

Installation prompt, all green and nice:

enter image description here

Normi answered 3/8, 2018 at 10:48 Comment(0)
C
3

If you're looking for something more ready for an Azure DevOps CI/CD pipeline, I've taken Joe Pitt's work and refactored it for my pipeline. It's up on github here

You can pass the script a pmx file path and password and it will tweak the certificate, install it, and sign the executable, setup, manifest and application files.

Please help me make it better :)

Chivalry answered 6/12, 2018 at 0:15 Comment(0)
C
1

A slight variant of this problem arises now with sha384 code signing certificates. If you sign your click once deployment with a sha384 code signing certificate in any Visual Studio version prior to VS 2022, then the signed click once deployment will have the "unknown publisher" problem. Due to a bug in mage.exe

mage.exe has been fixed in Visual Studio 2022. I upgraded to VS 2022 17.3 and the deployment is now signed correctly using my new sha384 code signing certificate.

Research regarding this problem brought me to issue 6732 from the MS developer team and it was marked as fixed in milestone VS17 which is VS 2022. Therefore, I don't think that MS will fix it for older versions of Visual Studio. https://github.com/dotnet/msbuild/issues/6732

Colmar answered 16/8, 2022 at 19:7 Comment(0)
D
0

The marked answer, for me anyway, results in a Smart Screen warning. You may be interested in the PowerShell script I have written, which fixes both issues by signing what it can with a SHA256 Certificate and then the ClickOnce (.application) files with a SHA1 Certificate.

SignClickOnceApp.ps1

Code at time of posting

<#
.SYNOPSIS 
    A PowerShell Script to correctly sign a ClickOnce Application.
.DESCRIPTION 
    Microsoft ClickOnce Applications Signed with a SHA256 Certificate show as Unknown Publisher during installation, ClickOnce Applications signed with a SHA1 Certificate show an Unknown Publisher SmartScreen Warning once installed, this happens because:
    1) The ClickOnce installer only supports SHA1 certificates (not SHA256), but,
    2) Microsoft has depreciated SHA1 for Authenticode Signing.

    This script uses two code signing certificates (one SHA1 and one SHA256) to sign the various parts of the ClickOnce Application so that both the ClickOnce Installer and SmartScreen are happy.
.PARAMETER VSRoot
    The Visual Studio Projects folder, if not provided .\Documents\Visual Studio 2015\Projects will be assumed
.PARAMETER SolutionName
    The Name of the Visual Studio Solution (Folder), if not provided the user is prompted.
.PARAMETER ProjectName
    The Name of the Visual Studio Project (Folder), if not provided the user is prompted.
.PARAMETER SHA1CertThumbprint
    The Thumbprint of the SHA1 Code Signing Certificate, if not provided the user is prompted.
.PARAMETER SHA256CertThumbprint
    The Thumbprint of the SHA256 Code Signing Certificate, if not provided the user is prompted.
.PARAMETER TimeStampingServer
    The Time Stamping Server to be used while signing, if not provided the user is prompted.
.PARAMETER PublisherName
    The Publisher to be set on the ClickOnce files, if not provided the user is prompted.
.PARAMETER Verbose
    Writes verbose output.
.EXAMPLE
    SignClickOnceApp.ps1 -VSRoot "C:\Users\Username\Documents\Visual Studio 2015\Projects" -SolutionName "MySolution" -ProjectName "MyProject" -SHA1CertThumbprint "f3f33ccc36ffffe5baba632d76e73177206143eb" -SHA256CertThumbprint "5d81f6a4e1fb468a3b97aeb3601a467cdd5e3266" -TimeStampingServer "http://time.certum.pl/" -PublisherName "Awesome Software Inc."
    Signs MyProject in MySolution which is in C:\Users\Username\Documents\Visual Studio 2015\Projects using the specified certificates, with a publisher of "Awesome Software Inc." and the Certum Timestamping Server.
.NOTES 
    Author  : Joe Pitt
    License : SignClickOnceApp by Joe Pitt is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/4.0/.
.LINK 
    https://www.joepitt.co.uk/Project/SignClickOnceApp/
#>
param (
    [string]$VSRoot, 
    [string]$SolutionName, 
    [string]$ProjectName, 
    [string]$SHA1CertThumbprint, 
    [string]$SHA256CertThumbprint, 
    [string]$TimeStampingServer,
    [string]$PublisherName,
    [switch]$Verbose
)

$oldverbose = $VerbosePreference
if($Verbose) 
{
    $VerbosePreference = "continue" 
}

# Visual Studio Root Path
if(!$PSBoundParameters.ContainsKey('VSRoot'))
{
    $VSRoot = '.\Documents\Visual Studio 2015\Projects\'
}
if (Test-Path "$VSRoot")
{
    Write-Verbose "Using '$VSRoot' for Visual Studio Root"
}
else
{
    Write-Error -Message "VSRoot does not exist." -RecommendedAction "Check path and try again" -ErrorId "1" `
        -Category ObjectNotFound -CategoryActivity "Testing VSRoot Path" -CategoryReason "The VSRoot path was not found" `
        -CategoryTargetName "$VSRoot" -CategoryTargetType "Directory"
    exit 1
}

# Solution Path
if(!$PSBoundParameters.ContainsKey('SolutionName'))
{
    $SolutionName = Read-Host "Solution Name"
}
if (Test-Path "$VSRoot\$SolutionName")
{
    Write-Verbose "Using '$VSRoot\$SolutionName' for Solution Path"
    $SolutionPath = "$VSRoot\$SolutionName"
}
else
{
    Write-Error -Message "Solution does not exist." -RecommendedAction "Check Solution Name and try again" -ErrorId "2" `
        -Category ObjectNotFound -CategoryActivity "Testing Solution Path" -CategoryReason "The Solution path was not found" `
        -CategoryTargetName "$VSRoot\$SolutionName" -CategoryTargetType "Directory"
    exit 2
}

# Project Path
if(!$PSBoundParameters.ContainsKey('ProjectName'))
{
    $ProjectName = Read-Host "Project Name"
}
if (Test-Path "$SolutionPath\$ProjectName")
{
    Write-Verbose "Using '$SolutionPath\$ProjectName' for Project Path"
    $ProjectPath = "$SolutionPath\$ProjectName"
}
else
{
    Write-Error -Message "Project does not exist." -RecommendedAction "Check Project Name and try again" -ErrorId "3" `
        -Category ObjectNotFound -CategoryActivity "Testing Project Path" -CategoryReason "The Project path was not found" `
        -CategoryTargetName "$SolutionPath\$ProjectName" -CategoryTargetType "Directory"
    exit 3
}

# Publish Path
if (Test-Path "$ProjectPath\publish")
{
    Write-Verbose "Using '$ProjectPath\publish' for Publish Path"
    $PublishPath = "$ProjectPath\publish"
}
else
{
    Write-Error -Message "Publish path does not exist." -RecommendedAction "Check Project has been published to \publish and try again" -ErrorId "4" `
        -Category ObjectNotFound -CategoryActivity "Testing Publish Path" -CategoryReason "The publish path was not found" `
        -CategoryTargetName "$ProjectPath\publish" -CategoryTargetType "Directory"
    exit 4
}

# Application Files Path
if (Test-Path "$PublishPath\Application Files")
{
    Write-Verbose "Using '$PublishPath\Application Files' for Application Files Path"
    $AppFilesPath = "$PublishPath\Application Files"
}
else
{
    Write-Error -Message "Application Files path does not exist." -RecommendedAction "Check Project has been published to \publish and try again" -ErrorId "5" `
        -Category ObjectNotFound -CategoryActivity "Testing Application Files Path" -CategoryReason "The Application Files path was not found" `
        -CategoryTargetName "$PublishPath\Application Files" -CategoryTargetType "Directory"
    exit 5
}

# Target Path
$TargetPath = Convert-Path "$AppFilesPath\${ProjectName}_*"
if ($($TargetPath.Length) -ne 0)
{
    Write-Verbose "Using $TargetPath for Target Path"
}
else
{
    Write-Error -Message "No versions." -RecommendedAction "Check Project has been published to \publish and try again" -ErrorId "6" `
        -Category ObjectNotFound -CategoryActivity "Searching for published version path" -CategoryReason "No Application has been published using ClickOnce" `
        -CategoryTargetName "$AppFilesPath\${ProjectName}_*" -CategoryTargetType "Directory"
    exit 6
}

# SHA1 Certificate
if(!$PSBoundParameters.ContainsKey('SHA1CertThumbprint'))
{
    $SHA1CertThumbprint = Read-Host "SHA1 Certificate Thumbprint"
}
if ("$SHA1CertThumbprint" -notmatch "^[0-9A-Fa-f]{40}$")
{
    Write-Error -Message "SHA1 Thumbprint Malformed" -RecommendedAction "Check the thumbprint and try again" -ErrorId "7" `
        -Category InvalidArgument -CategoryActivity "Verifying Thumbprint Format" -CategoryReason "Thumbprint is not a 40 character Base64 string" `
        -CategoryTargetName "$SHA1CertThumbprint" -CategoryTargetType "Base64String"
    exit 7
}
$SHA1Found = Get-ChildItem -Path Cert:\CurrentUser\My | where {$_.Thumbprint -eq "$SHA1CertThumbprint"} | Measure-Object
if ($SHA1Found.Count -eq 0)
{
    Write-Error -Message "SHA1 Certificate Not Found" -RecommendedAction "Check the thumbprint and try again" -ErrorId "8" `
        -Category ObjectNotFound -CategoryActivity "Searching for certificate" -CategoryReason "Certificate with Thumbprint not found" `
        -CategoryTargetName "$SHA1CertThumbprint" -CategoryTargetType "Base64String"
    exit 8
}

# SHA256 Certificate
if(!$PSBoundParameters.ContainsKey('SHA256CertThumbprint'))
{
    $SHA256CertThumbprint = Read-Host "SHA256 Certificate Thumbprint"
}
if ("$SHA256CertThumbprint" -notmatch "^[0-9A-Fa-f]{40}$")
{
    Write-Error -Message "SHA256 Thumbprint Malformed" -RecommendedAction "Check the thumbprint and try again" -ErrorId "9" `
        -Category InvalidArgument -CategoryActivity "Verifying Thumbprint Format" -CategoryReason "Thumbprint is not a 40 character Base64 string" `
        -CategoryTargetName "$SHA256CertThumbprint" -CategoryTargetType "Base64String"
    exit 9
}
$SHA256Found = Get-ChildItem -Path Cert:\CurrentUser\My | where {$_.Thumbprint -eq "$SHA256CertThumbprint"} | Measure-Object
if ($SHA256Found.Count -eq 0)
{
    Write-Error -Message "SHA256 Certificate Not Found" -RecommendedAction "Check the thumbprint and try again" -ErrorId "10" `
        -Category ObjectNotFound -CategoryActivity "Searching for certificate" -CategoryReason "Certificate with Thumbprint not found" `
        -CategoryTargetName "$SHA256CertThumbprint" -CategoryTargetType "Base64String"
    exit 10
}

# TimeStamping Server
if(!$PSBoundParameters.ContainsKey('TimeStampingServer'))
{
    $TimeStampingServer = Read-Host "TimeStamping Server URL"
}
if ("$TimeStampingServer" -notmatch "^http(s)?:\/\/[A-Za-z0-9-._~:/?#[\]@!$&'()*+,;=]+$")
{
    Write-Error -Message "SHA256 Thumbprint Malformed" -RecommendedAction "Check the TimeStamp URL and try again" -ErrorId "11" `
        -Category InvalidArgument -CategoryActivity "Verifying TimeStamping URL" -CategoryReason "TimeStamping URL is not a RFC Compliant URL" `
        -CategoryTargetName "$TimeStampingServer" -CategoryTargetType "URL"
    exit 11
}

# Publisher Name
# Project Path
if(!$PSBoundParameters.ContainsKey('PublisherName'))
{
    $PublisherName = Read-Host "Publisher Name"
}

# Sign setup.exe and application.exe with SHA256 Cert
Write-Verbose "Signing '$PublishPath\Setup.exe' (SHA256)"
Start-Process "$PSScriptRoot\signtool.exe" -ArgumentList "sign /fd SHA256 /td SHA256 /tr $TimeStampingServer /sha1 $SHA256CertThumbprint `"$PublishPath\Setup.exe`"" -Wait -NoNewWindow
Write-Verbose "Signing '$TargetPath\$ProjectName.exe.deploy' (SHA256)"
Start-Process "$PSScriptRoot\signtool.exe" -ArgumentList "sign /fd SHA256 /td SHA256 /tr $TimeStampingServer /sha1 $SHA256CertThumbprint `"$TargetPath\$ProjectName.exe.deploy`"" -Wait -NoNewWindow

# Remove .deploy extensions
Write-Verbose "Removing .deploy extensions"
Get-ChildItem "$TargetPath\*.deploy" -Recurse | Rename-Item -NewName { $_.Name -replace '\.deploy','' } 

# Sign Manifest with SHA256 Cert
Write-Verbose "Signing '$TargetPath\$ProjectName.exe.manifest' (SHA256)"
Start-Process "$PSScriptRoot\mage.exe" -ArgumentList "-update `"$TargetPath\$ProjectName.exe.manifest`" -ch $SHA256CertThumbprint -if `"Logo.ico`" -ti `"$TimeStampingServer`"" -Wait -NoNewWindow

# Sign ClickOnces with SHA1 Cert
Write-Verbose "Signing '$TargetPath\$ProjectName.application' (SHA1)"
Start-Process "$PSScriptRoot\mage.exe" -ArgumentList "-update `"$TargetPath\$ProjectName.application`"  -ch $SHA1CertThumbprint -appManifest `"$TargetPath\$ProjectName.exe.manifest`" -pub `"$PublisherName`" -ti `"$TimeStampingServer`"" -Wait -NoNewWindow
Write-Verbose "Signing '$PublishPath\$ProjectName.application' (SHA1)"
Start-Process "$PSScriptRoot\mage.exe" -ArgumentList "-update `"$PublishPath\$ProjectName.application`" -ch $SHA1CertThumbprint -appManifest `"$TargetPath\$ProjectName.exe.manifest`" -pub `"$PublisherName`" -ti `"$TimeStampingServer`"" -Wait -NoNewWindow

# Readd .deply extensions
Write-Verbose "Re-adding .deploy extensions"
Get-ChildItem -Path "$TargetPath\*"  -Recurse | Where-Object {!$_.PSIsContainer -and $_.Name -notlike "*.manifest" -and $_.Name -notlike "*.application"} | Rename-Item -NewName {$_.Name + ".deploy"}
Deas answered 1/10, 2016 at 21:2 Comment(2)
Could you copy the powershell source into this answer along with the link should the link go defunct (also here)? This improves the answer. Also, you should not receive a SmartScreen warning following my answer as long as your ClickOnce setup.exe has been signed with a valid SHA256 cert. Welcome to SO, nice contribution!!Diuretic
@AdamCaviness Current Code added as requested. Ref a SHA2 signed setup.exe, this is only true if the user uses setup.exe, if they just use the AppName.application then a certificate warning is shown.Deas

© 2022 - 2024 — McMap. All rights reserved.