【问题标题】:How to Authenticode sign ClickOnce deployment with an EV SHA2 cert and avoid "Unknown Publisher"如何使用 EV SHA2 证书对 ClickOnce 部署进行身份验证并避免“未知发布者”
【发布时间】:2016-09-16 18:59:43
【问题描述】:

通过 Visual Studio 的项目“签名”设置页面对 ClickOnce 部署进行签名时,我指定了我们的 SHA2 (SHA256) EV Authenticode 证书并发布。

发布并尝试运行引导程序 (setup.exe) 后,我在 ClickOnce 对话框中看到“未知发布者”。

有问题的 EV 证书有效并在 eToken 硬件令牌上运行,使用 SafeNet 客户端工具与令牌进行通信。使用 signtool 对常规 PE 文件(exe 和 dll)进行签名总是会产生完全有效的程序集,并且发布者是已知的。 这只是 ClickOnce 部署的问题。此外,ClickOnce 部署的各个文件看起来完全有效,因为文件属性对话框的数字签名选项卡正确列出了引导程序 (setup.exe) 和后缀为“.deploy”的程序集文件。

此外,“.application”和“.manifest”文件被适当地修改(可能通过 Visual Studio 的 mage)以包含 <publisherIdentity> 元素以及正确设置的算法。

签名机运行的是 Win10,我已经尝试了我能想象到的所有排列:

  • 有和没有时间戳
  • 有和没有强名称签名
  • 有和没有在线出版
  • 有和没有 https 在线发布
  • 通过发布页面有和没有特定的“更新位置”
  • 通过“发布”页面中的说明设置和不设置“发布者名称”
  • 使用清单选项的每个组合:
    • 排除部署提供程序 URL
    • 阻止应用程序通过 URL 激活
    • 使用应用程序清单获取信任信息
  • 不同版本的 Windows 上的多台机器
  • 通过 mage 和 signtool 进行手动清单签名和程序集签名(是的,mageui 也是如此)
  • 确保证书不会被证书提供者吊销

似乎有someone else experiencing this

【问题讨论】:

  • VisualStudio 不支持使用硬件令牌的 ClickOnce 签名,请使用 Mage.ese

标签: clickonce authenticode


【解决方案1】:

发生这种情况的原因是由于几个因素

  1. 使用 SHA2 Authenticode 证书时,ClickOnce 显示“未知发布者”。
  2. 2016 年 1 月 1 日,Windows 弃用了用于 Authenticode 签名/代码签名的 SHA1。因此,Windows SmartScreen 技术在使用 SHA1 Authenticode 证书时会显示“Unknown Publisher”。

这实际上是一个 catch-22,您需要 SHA1 用于 ClickOnce 发布者验证和 SHA2 用于 SmartScreen。 不错

与您的证书提供商(希望是真正的 CA)合作,为您获取 SHA1 SHA2 证书。 DigiCert 的人很棒。在大多数情况下,您必须与您的 CA 合作,因为即使您已经拥有自己的 SHA2 证书并且您与他们合作以获得 SHA1 证书(反之亦然),它可能会自动撤销您拥有的任何现有证书.在 DigiCert 的情况下,当我解释我想尝试什么(双重签名)时,他们能够阻止自动撤销。

在您的 EV 令牌上安装这些后,配置 Visual Studio 以使用您的 SHA1 证书对 ClickOnce 清单进行签名。理想情况下,您还将在同一个对话框中提供时间戳服务器,以确保您的证书最终到期。

在本地发布 ClickOnce 部署后和分发之前,通过附加 SHA2 证书对 ClickOnce 引导程序 (setup.exe) 进行双重签名。

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

请注意,查找证书指纹的一种方法是通过证书 MMC 管理单元。是的,对于 SHA2 证书,指纹应该是 SHA1。

现在,引导程序会在文件属性对话框的“数字签名”选项卡中显示您的两个证书。

当您从指定为 Visual Studio 中发布页面的“安装文件夹 URL”的位置运行 setup.exe 时,您应该会看到发布者是受信任的。了解安装文件夹很重要,因为如果您要从另一个位置运行应用程序,您应该预计不可信,因为引导程序会调用已知的安装文件夹来检索应用程序文件。

【讨论】:

  • 我尝试按照这些指示进行操作。我联系了 COMODO,他们在大约一个月前颁发了我的 SHA2 EV 证书。他们说,“我们不再颁发 SHA1 证书。”很明显,我正在与一个无法升级问题的工作人员聊天。我不知道下一步该做什么。
  • @RobPerkins 有点老了,但你在这方面有什么进展吗?我的情况与 USB 令牌上的 GlobalSign EV 证书相同......
  • 没什么。我将不得不经历昂贵的过程,使用将产生两种证书的证书提供者。现在不值得。
【解决方案2】:

似乎从 Visual Studio 15.7.5(或者可能是以前的版本,我没有检查它们)开始,当使用 SHA2 EV 代码签名证书签名时,setup.exe 和应用程序二进制文件都对 ClickOnce 有效(无需询问您的 SHA-1 证书提供商)。我使用的是 Windows 10 (10.0.16299.492),我们也在 Windows 8 上进行了检查,两者都可以正常工作。我不知道这是否是 Visual Studio 或 SmartScreen 更新版本的影响。一年前我未能发布签名的 ClickOnce 应用程序,现在一切正常。

主要应用项目签名属性:

“从商店选择”对话框:

已发布 ClickOnce setup.exe 属性

已发布 ClickOnce 应用程序 *.exe.deploy 文件属性

安装提示,全绿好看:

【讨论】:

    【解决方案3】:

    如果您正在寻找更适合 Azure DevOps CI/CD 管道的东西,我已采用 Joe Pitt 的工作并将其重构为我的管道。它在github上here

    您可以向脚本传递 pmx 文件路径和密码,它会调整证书、安装证书,并对可执行文件、设置、清单和应用程序文件进行签名。

    请帮我把它做得更好:)

    【讨论】:

      【解决方案4】:

      无论如何,对我来说,标记的答案会导致智能屏幕警告。您可能对我编写的 PowerShell 脚本感兴趣,该脚本通过使用 SHA256 证书签署它可以解决的两个问题,然后使用 SHA1 证书签署 ClickOnce (.application) 文件。

      SignClickOnceApp.ps1

      发布时的代码

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

      【讨论】:

      • 如果链接失效(也是here),您能否将powershell 源代码连同链接一起复制到此答案中?这改善了答案。此外,只要您的 ClickOnce setup.exe 已使用 有效 SHA256 证书签名,您就不会在我的回答之后收到 SmartScreen 警告。欢迎来到 SO,很好的贡献!
      • @AdamCaviness 当前代码已按要求添加。引用一个 SHA2 签名的 setup.exe,这仅在用户使用 setup.exe 时才成立,如果他们只使用 AppName.application,则会显示证书警告。
      猜你喜欢
      • 2014-10-15
      • 2020-06-27
      • 2023-03-29
      • 1970-01-01
      • 2019-07-20
      • 1970-01-01
      • 2018-11-26
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多