自动化扩展验证 (EV) 代码签名

Posted

技术标签:

【中文标题】自动化扩展验证 (EV) 代码签名【英文标题】:Automate Extended Validation (EV) code signing 【发布时间】:2013-07-29 15:21:39 【问题描述】:

我们最近购买了 DigiCert EV 代码签名证书。我们可以使用 signtool.exe 对 .exe 文件进行签名。但是,每次我们签署文件时,它都会提示输入 SafeNet eToken 密码。

我们如何在无需用户干预的情况下通过在某处存储/缓存密码来自动化此过程?

【问题讨论】:

问题“How safe are the password prompts of the SafeNet eToken 5110 or similar cryptographic hardware tokens?”有点相关,如果它得到答案,那些评估是否自动输入密码的人应该会感兴趣。就我而言,如果当前拥有该令牌或类似令牌的人读到此,如果您可以尝试“破解”它并回答该问题,将不胜感激:) 不幸的是,对我有用且得票最多的答案出现在答案列表的末尾,所以不要浪费时间直接去 Simon Mourier 的答案***.com/a/26126701/27194 在尝试任何这些解决方案之前请注意。硬件令牌有一个“剩余令牌密码重试次数”计数器(可以在 SafeNet 身份验证客户端中检查)。进行实验时,请确保它永远不会出于明显的原因达到零。否则,您可能会被永久锁定在硬件令牌之外,您将不得不订购一个新的!学会了这个艰难的方式...... 不幸的是,西蒙的答案不再有效(请参阅my comment to the answer)。奥斯汀的答案不仅有效,而且无论如何都更好。 method described by Austin Morton 就像一个魅力,但非常重要的是要注意它需要signtool.exe最新版本。使用过时版本(我的是 2016 年)我收到错误 ``` 错误信息:“CryptExportPublicKeyInfoEx 失败”(87/0x57)```您可以通过安装 @ 获得最新版本987654325@。至少在撰写本文时,SDK 提供的版本支持使用 [Austin Mor 描述的方法 【参考方案1】:

无法绕过登录对话框 AFAIK,但您可以配置 SafeNet 身份验证客户端,使其在每个登录会话中只询问一次。

我在这里引用 SAC 文档(在\ProgramFiles\SafeNet\Authentication\SAC\SACHelp.chm 中找到,章节'Client Settings','Enabling Client Logon'):

启用单点登录后,用户可以访问多个应用程序 在每台计算机期间仅请求一次令牌密码 会议。这减轻了用户登录到每个 单独申请。

要启用此默认禁用的功能,请转到 SAC 高级设置,然后选中“启用单一登录”框:

重新启动您的计算机,它现在应该只提示输入令牌密码一次。在我们的例子中,每个构建都有 200 多个二进制文件要签名,所以这是一个总体必须

否则,这是一个小的 C# 控制台示例代码(相当于 m1st0 之一),它允许您自动响应登录对话框(可能需要以管理员身份运行)(您需要从您的控制台项目中引用(UIAutomationClient.dllUIAutomationTypes.dll):

using System;
using System.Windows.Automation;

namespace AutoSafeNetLogon 
   class Program 
      static void Main(string[] args) 
         SatisfyEverySafeNetTokenPasswordRequest("YOUR_TOKEN_PASSWORD");
      


      static void SatisfyEverySafeNetTokenPasswordRequest(string password) 
         int count = 0;
         Automation.AddAutomationEventHandler(WindowPattern.WindowOpenedEvent, AutomationElement.RootElement, TreeScope.Children, (sender, e) =>
         
            var element = sender as AutomationElement;
            if (element.Current.Name == "Token Logon") 
               WindowPattern pattern = (WindowPattern)element.GetCurrentPattern(WindowPattern.Pattern);
               pattern.WaitForInputIdle(10000);
               var edit = element.FindFirst(TreeScope.Descendants, new AndCondition(
                   new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Edit),
                   new PropertyCondition(AutomationElement.NameProperty, "Token Password:")));

               var ok = element.FindFirst(TreeScope.Descendants, new AndCondition(
                   new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Button),
                   new PropertyCondition(AutomationElement.NameProperty, "OK")));

               if (edit != null && ok != null) 
                  count++;
                  ValuePattern vp = (ValuePattern)edit.GetCurrentPattern(ValuePattern.Pattern);
                  vp.SetValue(password);
                  Console.WriteLine("SafeNet window (count: " + count + " window(s)) detected. Setting password...");

                  InvokePattern ip = (InvokePattern)ok.GetCurrentPattern(InvokePattern.Pattern);
                  ip.Invoke();
                else 
                  Console.WriteLine("SafeNet window detected but not with edit and button...");
               
            
         );

         do 
            // press Q to quit...
            ConsoleKeyInfo k = Console.ReadKey(true);
            if (k.Key == ConsoleKey.Q)
               break;
         
         while (true);
         Automation.RemoveAllEventHandlers();
      
   

【讨论】:

这可能不是来自 DigiCert 的官方答案,但他们的答案很糟糕,而且这个太棒了!感谢您的帮助! +1 以获得正确答案。看到人们开发脚本来自动化用户输入等,这让我感到惊讶,这违背了真正拥有密码的目的,他们只需要知道这个选项在哪里。我怀疑这个选项会永远消失,因为发行者明白开发人员不能在每次签署二进制文件时都输入密码。 我可以确认这适用于 TeamCity(只要 TeamCity Windows 服务勾选了“允许服务与桌面交互”框)。我们还需要在另一个线程中运行密码输入过程,并在我们的构建机器上禁用“交互式服务检测”服务。我们围绕 signtool 创建了一个 C# 包装器,它执行签名并处理上述密码输入,所有这些都在一个自包含的应用程序中。我不敢相信我们必须跨越多少障碍才能让这项工作发挥作用,但对于同一条船上的其他人,请专注于上述 C# 方法...... 仅供参考......赛门铁克 EV 证书也使用 SafeNet。我们不得不围绕这个过程构建一个 janky 解决方案,但是在阅读了您的答案并实现了控制台应用程序之后,这极大地帮助了我们的构建过程。谢谢你。解决架构不佳的代码签名过程的绝佳解决方案。 我已经成功使用这个有用的解决方案一段时间了。但是现在,当在 Windows 10 Pro 2004 和 Windows 8 及更高版本的 SafeNet 客户端 9.0.34 x64 的新机器上设置它时,它不再工作了。新密码提示在哪里。它似乎是 Windows 内置的,而不是像以前那样自定义 SafeNet 提示。并且新提示的密码框是不可自动化的(它没有暴露在AutomationElement树中)。我不知道它是否可以以某种方式解决。但是再次访问这个问题并通过@Austin 找到答案,我相信无论如何这是一个更好的解决方案。【参考方案2】:

扩展此线程中已有的答案,可以使用 microsoft 的标准 signtool 程序提供令牌密码。

0。在高级视图中打开 SafeNet 客户端

安装路径可能不同,但对我来说,SafeNet 客户端安装到:C:\Program Files\SafeNet\Authentication\SAC\x64\SACTools.exe

点击右上角的齿轮图标打开“高级视图”。

1.将您的公共证书从 SafeNet 客户端导出到文件

2。查找您的私钥容器名称

3.找到您的读者姓名

4.一起格式化

eToken CSP 具有隐藏(或至少没有广泛宣传)的功能,可以从容器名称中解析令牌密码。

格式为以下之一

[]=name
[reader]=name
[password]=name
[readerpassword]=name

地点:

reader 是 SafeNet 客户端 UI 中的“读者名称” password 是您的令牌密码 name 是 SafeNet 客户端 UI 中的“容器名称”

如果您连接了多个阅读器,您可能必须指定阅读器名称 - 因为我只有一个阅读器,我无法确认这一点。

5.将信息传递给signtool

/f certfile.cer /csp "eToken Base Cryptographic Provider" /k "<value from step 4>" 您需要的任何其他 signtool 标志

signtool 命令示例如下

signtool sign /f mycert.cer /csp "eToken Base Cryptographic Provider" /k "[TokenPasswordHere]=KeyContainerNameHere" myfile.exe

一些来自这个答案的图片:https://***.com/a/47894907/5420193

【讨论】:

效果很好,遗憾的是我在两天的挖掘和实施自己的“signtool”后发现了这个:D thx 警告:如果您输入错误的密码,此解决方案可能会将您锁定在硬件令牌之外,即使只是两次!不知何故,在我使用无效密码执行一次命令后,密码重试次数剩余计数器从 15 减少到 3。 “etokensign.exe”解决方案似乎工作正常,但在一次无效密码尝试后,剩余密码计数器应从 15 减少到 14。 太棒了,这对我来说非常适合 Sectigo 提供的 SafeNet USB 加密狗。 据我所知,这种语法在任何地方都没有公开记录。我通过在 IDA Pro 中对驱动程序二进制文件进行逆向工程发现了这个功能。 这比其他向 GUI 提示输入假密码的答案要好。除此之外,这甚至在非交互式 Windows 会话中也有效。还有其他答案seem not to work with the latest tools。【参考方案3】:

在this answer 上扩展,这可以使用CryptAcquireContext 和CryptSetProvParam 以编程方式输入令牌PIN 和CryptUIWizDigitalSign 以编程方式执行签名来自动化。我创建了一个控制台应用程序(下面的代码),它将证书文件(通过右键单击 SafeNet 身份验证客户端中的证书并选择“导出...”导出)、私钥容器名称(在 SafeNet 身份验证客户端中找到)作为输入,令牌 PIN、时间戳 URL 和要签名的文件的路径。当连接 USB 令牌的 TeamCity 构建代理调用此控制台应用程序时,该应用程序可以正常工作。

用法示例:etokensign.exe c:\CodeSigning.cert CONTAINER PIN http://timestamp.digicert.com C:\program.exe

代码:

#include <windows.h>
#include <cryptuiapi.h>
#include <iostream>
#include <string>

const std::wstring ETOKEN_BASE_CRYPT_PROV_NAME = L"eToken Base Cryptographic Provider";

std::string utf16_to_utf8(const std::wstring& str)

    if (str.empty())
    
        return "";
    

    auto utf8len = ::WideCharToMultiByte(CP_UTF8, 0, str.data(), str.size(), NULL, 0, NULL, NULL);
    if (utf8len == 0)
    
        return "";
    

    std::string utf8Str;
    utf8Str.resize(utf8len);
    ::WideCharToMultiByte(CP_UTF8, 0, str.data(), str.size(), &utf8Str[0], utf8Str.size(), NULL, NULL);

    return utf8Str;


struct CryptProvHandle

    HCRYPTPROV Handle = NULL;
    CryptProvHandle(HCRYPTPROV handle = NULL) : Handle(handle) 
    ~CryptProvHandle()  if (Handle) ::CryptReleaseContext(Handle, 0); 
;

HCRYPTPROV token_logon(const std::wstring& containerName, const std::string& tokenPin)

    CryptProvHandle cryptProv;
    if (!::CryptAcquireContext(&cryptProv.Handle, containerName.c_str(), ETOKEN_BASE_CRYPT_PROV_NAME.c_str(), PROV_RSA_FULL, CRYPT_SILENT))
    
        std::wcerr << L"CryptAcquireContext failed, error " << std::hex << std::showbase << ::GetLastError() << L"\n";
        return NULL;
    

    if (!::CryptSetProvParam(cryptProv.Handle, PP_SIGNATURE_PIN, reinterpret_cast<const BYTE*>(tokenPin.c_str()), 0))
    
        std::wcerr << L"CryptSetProvParam failed, error " << std::hex << std::showbase << ::GetLastError() << L"\n";
        return NULL;
    

    auto result = cryptProv.Handle;
    cryptProv.Handle = NULL;
    return result;


int wmain(int argc, wchar_t** argv)

    if (argc < 6)
    
        std::wcerr << L"usage: etokensign.exe <certificate file path> <private key container name> <token PIN> <timestamp URL> <path to file to sign>\n";
        return 1;
    

    const std::wstring certFile = argv[1];
    const std::wstring containerName = argv[2];
    const std::wstring tokenPin = argv[3];
    const std::wstring timestampUrl = argv[4];
    const std::wstring fileToSign = argv[5];

    CryptProvHandle cryptProv = token_logon(containerName, utf16_to_utf8(tokenPin));
    if (!cryptProv.Handle)
    
        return 1;
    

    CRYPTUI_WIZ_DIGITAL_SIGN_EXTENDED_INFO extInfo = ;
    extInfo.dwSize = sizeof(extInfo);
    extInfo.pszHashAlg = szOID_NIST_sha256; // Use SHA256 instead of default SHA1

    CRYPT_KEY_PROV_INFO keyProvInfo = ;
    keyProvInfo.pwszContainerName = const_cast<wchar_t*>(containerName.c_str());
    keyProvInfo.pwszProvName = const_cast<wchar_t*>(ETOKEN_BASE_CRYPT_PROV_NAME.c_str());
    keyProvInfo.dwProvType = PROV_RSA_FULL;

    CRYPTUI_WIZ_DIGITAL_SIGN_CERT_PVK_INFO pvkInfo = ;
    pvkInfo.dwSize = sizeof(pvkInfo);
    pvkInfo.pwszSigningCertFileName = const_cast<wchar_t*>(certFile.c_str());
    pvkInfo.dwPvkChoice = CRYPTUI_WIZ_DIGITAL_SIGN_PVK_PROV;
    pvkInfo.pPvkProvInfo = &keyProvInfo;

    CRYPTUI_WIZ_DIGITAL_SIGN_INFO signInfo = ;
    signInfo.dwSize = sizeof(signInfo);
    signInfo.dwSubjectChoice = CRYPTUI_WIZ_DIGITAL_SIGN_SUBJECT_FILE;
    signInfo.pwszFileName = fileToSign.c_str();
    signInfo.dwSigningCertChoice = CRYPTUI_WIZ_DIGITAL_SIGN_PVK;
    signInfo.pSigningCertPvkInfo = &pvkInfo;
    signInfo.pwszTimestampURL = timestampUrl.c_str();
    signInfo.pSignExtInfo = &extInfo;

    if (!::CryptUIWizDigitalSign(CRYPTUI_WIZ_NO_UI, NULL, NULL, &signInfo, NULL))
    
        std::wcerr << L"CryptUIWizDigitalSign failed, error " << std::hex << std::showbase << ::GetLastError() << L"\n";
        return 1;
    

    std::wcout << L"Successfully signed " << fileToSign << L"\n";
    return 0;

将证书导出到文件:

私钥容器名称:

【讨论】:

这个应该是公认的答案,它就像一个魅力! 这是完美的。特别是在我意识到我只需要用这个工具签署一些虚拟文件之后。如果启用了“单一登录”(SafeNet 驱动程序),则所有后续步骤都使用标准 signtool。这对于签署使用不同工具的 Office 插件 (VSTO) 非常有用,这也意味着我的构建脚本/过程只需要进行最少的更改。 这个答案是对 avzhatkin 提供的答案的一个很好的补充。此时代码已接近替换signtools.exe。该程序需要支持交叉签名。幸运的是,现在还有另一个 SO 帖子要执行 cross signing。 这个答案最终对我帮助最大。我在 VS2017 中构建时错过了外部参考,但是当按照建议 here 添加一些 pragma cmets 时,我设法让 Bamboo(Atlassian 的 CI/CD)签名。 链接到“Cryptui.lib”以成功构建。对于 Visual C++,在 #includes 之后插入 #pragma comment(lib, "Cryptui.lib") 就足够了。【参考方案4】:

我是一个测试版工具,这将有助于自动化构建过程。

它是客户端-服务器窗口应用程序。您可以在插入 EV 令牌的计算机上启动服务器。在服务器端应用程序启动时输入令牌密码。在此之后,您可以远程签署文件。 客户端应用程序完全取代了 signtool.exe,因此您可以使用现有的构建脚本。

源代码位于此处:https://github.com/SirAlex/RemoteSignTool

编辑:我们在过去半年 24x7 的构建服务器上成功使用此工具进行代码签名。一切正常。

【讨论】:

这种方法的安全性如何?这是否意味着任何可以使用 HTTP 连接到您的签名服务器的人都可以使用您的 EV 证书签署他们想要的任何二进制文件?【参考方案5】:

signtool.exe sign /fd sha256 /f "signing.cer" /csp "eToken Base Cryptographic Provider" /kc "[token password here]=Container name here" "ConsoleApp1.exe"

将 Microsoft Windows SDK 10 用于 signtool

【讨论】:

太棒了!这个单线当然让所有其他答案都感到羞耻(尽管起初我很困惑如何找出我的“容器名称”,直到我在上面 draketb 的答案中找到了说明)。【参考方案6】:

实际上,在 Windows 上,您可以完全以编程方式指定令牌密码。这可以通过使用“\\.\AKS ifdh 0”形式的令牌名称或令牌容器名称创建带有标志 CRYPT_SILENT 的上下文 (CryptAcquireContext) 来完成,这是身份验证客户端应用程序的证书属性中可见的一些 guid。然后您需要使用 CryptSetProvParam 和参数 PP_SIGNATURE_PIN 来指定您的令牌密码。之后,该进程可以使用该令牌上的证书对文件进行签名。注意:创建上下文后,它似乎完全适用于当前进程,无需将其传递给其他 Crypto API 函数或任何东西。但是,如果您发现需要更多努力的情况,请随时发表评论。编辑:添加的代码示例

HCRYPTPROV OpenToken(const std::wstring& TokenName, const std::string& TokenPin)

    const wchar_t DefProviderName[] = L"eToken Base Cryptographic Provider";

    HCRYPTPROV hProv = NULL;
    // Token naming can be found in "eToken Software Developer's Guide"
    // Basically you can either use "\\.\AKS ifdh 0" form
    // Or use token's default container name, which looks like "ab-c0473610-8e6f-4a6a-ae2c-af944d09e01c"
    if(!CryptAcquireContextW(&hProv, TokenName.c_str(), DefProviderName, PROV_RSA_FULL, CRYPT_SILENT))
    
        DWORD Error = GetLastError();
        //TracePrint("CryptAcquireContext for token %ws failed, error 0x%08X\n", TokenName.c_str(), Error);
        return NULL;
    
    if(!CryptSetProvParam(hProv, PP_SIGNATURE_PIN, (BYTE*)TokenPin.c_str(), 0))
    
        DWORD Error = GetLastError();
        //TracePrint("Token %ws unlock failed, error 0x%08X\n", TokenName.c_str(), Error);
        CryptReleaseContext(hProv, 0);
        return NULL;
    
    else
    
        //TracePrint("Unlocked token %ws\n", TokenName.c_str());
        return hProv;
    

【讨论】:

有趣。看起来很有希望,恕我直言,您应该详细说明(增强解释,提供代码等) 请发布一个完整的例子。这听起来真的很有用 感谢您提供更多详细信息。这是你提到的指南吗?read.pudn.com/downloads128/ebook/549477/eToken_SDK_3_50[1].pdf 我相信这不是我所拥有的确切版本,但它似乎包含有关创建上下文和提供 PIN 的类似信息,尽管适用于不同的使用场景。 我猜你称这个函数为 OpenToken(L"\\\\.\\AKS ifdh 0",)...它对我有用!【参考方案7】:

我使用AutoHotKey 使用以下脚本自动输入密码。我们一直在尝试为我们的开发人员创建一个基于 Web 的前端,以便在运行此脚本的情况下将二进制文件发送到 Windows 机器,以便对其进行签名和返回。

  Loop
     
    Sleep 2000

    if (WinExist("Token Logon"))
       
      WinActivate ; use the window found above
      SendInput [your_password]
      SendInput Enter
       
    if (WinExist("DigiCert Certificate Utility for Windows©"))
       
      WinActivate ; use the window found above
      SendInput [your_password]
      SendInput Enter
       
   

我必须指出,我分享的内容并非完全不安全,但我们也遇到了这个问题,需要为每个开发人员购买签名密钥或分配一个签名经理的工作来批准已发布软件的签名。我相信这些是更好、更安全的流程——一旦通过质量保证并获准发布,就可以正式签署。但是,较小的公司的需求可能会要求以其他一些自动化方式来完成。

我最初在 Linux 上使用osslsigncode(在 EV 证书之前)来自动签署 Windows 可执行文件(因为我们有一个 Linux 服务器为开发人员的轻松和协作做了很多工作)。我已经联系了 osslsigncode 的开发人员,看看他是否可以利用 DigiCert SafeNet 令牌以不同的方式帮助实现自动化,因为我可以在 Linux 上看到它们。他的回复带来了希望,但我不确定是否有任何进展,我无法投入更多时间来提供帮助

【讨论】:

查看其他答案。可以选择每个会话仅解锁一次,这对大多数用户来说已经足够了。【参考方案8】:

安装https://chocolatey.org/docs/installation (可以使用管理命令提示符下的一个命令来完成)

(重启命令提示符)

抑制每次安装时 choco 的不断提示:

choco feature enable -n=allowGlobalConfirmation

安装python,使用命令:

choco install python

(重新启动命令提示符) 安装额外的python模块:

pip install pypiwin32

将以下文本保存到disableAutoprompt.py

import pywintypes
import win32con
import win32gui
import time



DIALOG_CAPTION = 'Token Logon'
DIALOG_CLASS = '#32770'
PASSWORD_EDIT_ID = 0x3ea
TOKEN_PASSWORD_FILE = 'password.txt'
SLEEP_TIME = 10


def get_token_password():
    password = getattr(get_token_password, '_password', None)
    if password is None:
        with open(TOKEN_PASSWORD_FILE, 'r') as f:
            password = get_token_password._password = f.read()

    return password

def enumHandler(hwnd, lParam):
    if win32gui.IsWindowVisible(hwnd):
        if win32gui.GetWindowText(hwnd) == DIALOG_CAPTION and win32gui.GetClassName(hwnd) == DIALOG_CLASS:
            print('Token logon dialog has been detected, trying to enter password...')
            try:
                ed_hwnd = win32gui.GetDlgItem(hwnd, PASSWORD_EDIT_ID)
                win32gui.SendMessage(ed_hwnd, win32con.WM_SETTEXT, None, get_token_password())
                win32gui.PostMessage(ed_hwnd, win32con.WM_KEYDOWN, win32con.VK_RETURN, 0)
                print('Success.')
            except Exception as e:
                print('Fail: '.format(str(e)))
                return False

    return True


def main():
    while True:
        try:
            win32gui.EnumWindows(enumHandler, None)
            time.sleep(SLEEP_TIME)
        except pywintypes.error as e:
            if e.winerror != 0:
                raise e


if __name__ == '__main__':
    print('Token unlocker has been started...')
    print('DO NOT CLOSE THE WINDOW!')
    main()

将密码保存到 passwd.txt,然后运行

python disableAutoprompt.py

来自SafeNet Authentication Client - 配置 > Client Settings > Advanced > Enable Single Log On 可以启用选项以最小化密码提示的数量,但是 它不会完全禁用它们(在 10.4.26.0 版本上测试)

C# 应用程序(例如https://github.com/ganl/safenetpass)不能在锁定屏幕上工作,但可以在这个 python 脚本上工作。

【讨论】:

这个脚本很棒,我可以很容易地适应我使用 Yubikey 加密狗的需要。但是,Windows 10 打破了它。 Windows 10 更改为 XAML,因此 win32gui.xxxx() 函数将不起作用。 /叹。感谢微软。这就是为什么我们不能拥有美好的事物。【参考方案9】:

得到了 Digicert 的答复:

不幸的是,EV 代码签名证书的部分安全性在于您每次都必须输入密码。 没有办法让它自动化。

【讨论】:

我们得到了同样的回应,尽管他们正在寻找一种解决方案,但没有确定何时可用。尽管他们知道这篇 SO 帖子,但希望他们能意识到这是一个多么严重的问题。 我们找到了解决办法:github.com/mareklinka/SafeNetTokenSigner/issues/8【参考方案10】:

我的情况是 Digicert 为 CI 颁发标准 (OV) 证书,如果您已经拥有 EV 证书,则免费。

我知道这不是解决方案,但如果您不能将令牌放在服务器(云服务器)中,这就是要走的路。

【讨论】:

【参考方案11】:

我的做法是:

    打开令牌

    PCCERT_CONTEXT 证书 = OpenToken(SAFENET_TOKEN, EV_PASS);

    对文件进行签名,在需要时使用令牌、根/交叉证书以及加载到内存中的 EV 证书。

    HRESULT hr = SignAppxPackage(cert, FILETOSIGN);

使用 SignerSignEx2():

文件使用 SignerSignEx2() 签名,需要使用 LoadLibrary() 和 GetProcAddress() 加载到内存中:

// Type definition for invoking SignerSignEx2 via GetProcAddress
typedef HRESULT(WINAPI *SignerSignEx2Function)(
    DWORD,
    PSIGNER_SUBJECT_INFO,
    PSIGNER_CERT,
    PSIGNER_SIGNATURE_INFO,
    PSIGNER_PROVIDER_INFO,
    DWORD,
    PCSTR,
    PCWSTR,
    PCRYPT_ATTRIBUTES,
    PVOID,
    PSIGNER_CONTEXT *,
    PVOID,
    PVOID);

// Load the SignerSignEx2 function from MSSign32.dll
HMODULE msSignModule = LoadLibraryEx(
    L"MSSign32.dll",
    NULL,
    LOAD_LIBRARY_SEARCH_SYSTEM32);

if (msSignModule)

    SignerSignEx2Function SignerSignEx2 = reinterpret_cast<SignerSignEx2Function>(
        GetProcAddress(msSignModule, "SignerSignEx2"));
    if (SignerSignEx2)
    
        hr = SignerSignEx2(
            signerParams.dwFlags,
            signerParams.pSubjectInfo,
            signerParams.pSigningCert,
            signerParams.pSignatureInfo,
            signerParams.pProviderInfo,
            signerParams.dwTimestampFlags,
            signerParams.pszAlgorithmOid,
            signerParams.pwszTimestampURL,
            signerParams.pCryptAttrs,
            signerParams.pSipData,
            signerParams.pSignerContext,
            signerParams.pCryptoPolicy,
            signerParams.pReserved);
    
    else
    
        DWORD lastError = GetLastError();
        hr = HRESULT_FROM_WIN32(lastError);
    

    FreeLibrary(msSignModule);

else

    DWORD lastError = GetLastError();
    hr = HRESULT_FROM_WIN32(lastError);


// Free any state used during app package signing
if (sipClientData.pAppxSipState)

    sipClientData.pAppxSipState->Release();

时间戳

此外,您必须为您的签名文件加上时间戳,并使用您连接的时间戳授权来执行此操作。

这是通过 URL 安全地检查时间戳服务器的当前日期和时间来完成的。每个签名机构都有自己的时间戳服务器。时间戳是代码签名过程中的一个额外步骤,但是当涉及到 EV 代码签名时,它是为签名的 PE 添加额外的安全层的要求。 因此,请在您的代码中添加检查用户是否已连接到 Internet。

DWORD dwReturnedFlag;
if (InternetGetConnectedState(&dwReturnedFlag,0) == NULL) // use https://docs.microsoft.com/en-us/windows/desktop/api/netlistmgr/nf-netlistmgr-inetworklistmanager-getconnectivity

    wprintf(L"Certificate can't be dated with no Internet connection\n");
    return 1;

从文件加载证书

std::tuple<DWORD, DWORD, std::string> GetCertificateFromFile
(const wchar_t*                         FileName
    , std::shared_ptr<const CERT_CONTEXT>*   ResultCert)

    std::vector<unsigned char> vecAsn1CertBuffer;
    auto tuple_result = ReadFileToVector(FileName, &vecAsn1CertBuffer);

    if (std::get<0>(tuple_result) != 0)
    
        return tuple_result;
    

    return GetCertificateFromMemory(vecAsn1CertBuffer, ResultCert);

将证书加载到内存中

std::tuple<DWORD, DWORD, std::string> GetCertificateFromMemory
(const std::vector<unsigned char>&      CertData
    , std::shared_ptr<const CERT_CONTEXT>*   ResultCert)

    const CERT_CONTEXT* crtResultCert = ::CertCreateCertificateContext
    (X509_ASN_ENCODING | PKCS_7_ASN_ENCODING
        , &CertData[0]
        , static_cast<DWORD>(CertData.size()));
    if (crtResultCert == NULL)
    
        return std::make_tuple(E_FAIL
            , ::GetLastError()
            , "CertCreateCertificateContext");
    

    *ResultCert = std::shared_ptr<const CERT_CONTEXT>(crtResultCert
        , ::CertFreeCertificateContext);
    return std::make_tuple(0, 0, "");

在访问硬件令牌后加载证书后,我们加载它:

std::vector<unsigned char> dataCertEV(signingCertContext->pbCertEncoded,
        signingCertContext->pbCertEncoded + signingCertContext->cbCertEncoded);

最后,签名在以下函数中完成:

HRESULT SignAppxPackage(
    _In_ PCCERT_CONTEXT signingCertContext,
    _In_ LPCWSTR packageFilePath)

    HRESULT hr = S_OK;
    if (PathFileExists(CertAuthority_ROOT))
    
        wprintf(L"Cross Certificate '%s' was found\n", CertAuthority_ROOT);
    
    else
    
        wprintf(L"Error: Cross Certificate '%s' was not found\n", CertAuthority_ROOT);
        return 3;
    
    DWORD dwReturnedFlag;
    if (InternetGetConnectedState(&dwReturnedFlag,0) == NULL) 
    
        wprintf(L"Certificate can't be dated with no Internet connection\n");
        return 1;
    
    if (PathFileExists(CertAuthority_RSA))
    
        wprintf(L"Cross Certificate '%s' was found\n", CertAuthority_RSA);
    
    else
    
        wprintf(L"Error: Cross Certificate '%s' was not found\n", CertAuthority_RSA);
        return 2;
    
    if (PathFileExists(CROSSCERTPATH))
    
        wprintf(L"Microsoft Cross Certificate '%s' was found\n", CROSSCERTPATH);

    
    else
    
        wprintf(L"Error: Microsoft Cross Certificate '%s' was not found\n", CROSSCERTPATH);
        return 3;
    
    // Initialize the parameters for SignerSignEx2
    DWORD signerIndex = 0;

    SIGNER_FILE_INFO fileInfo = ;
    fileInfo.cbSize = sizeof(SIGNER_FILE_INFO);
    fileInfo.pwszFileName = packageFilePath;

    SIGNER_SUBJECT_INFO subjectInfo = ;
    subjectInfo.cbSize = sizeof(SIGNER_SUBJECT_INFO);
    subjectInfo.pdwIndex = &signerIndex;
    subjectInfo.dwSubjectChoice = SIGNER_SUBJECT_FILE;
    subjectInfo.pSignerFileInfo = &fileInfo;

    SIGNER_CERT_STORE_INFO certStoreInfo = ;
    certStoreInfo.cbSize = sizeof(SIGNER_CERT_STORE_INFO);
    certStoreInfo.dwCertPolicy = SIGNER_CERT_POLICY_STORE;// SIGNER_CERT_POLICY_CHAIN_NO_ROOT;
    certStoreInfo.pSigningCert = signingCertContext;

    // Issuer: 'CertAuthority RSA Certification Authority'
    // Subject 'CertAuthority RSA Extended Validation Code Signing CA'
    auto fileCertAuthorityRsaEVCA = CertAuthority_RSA;
    std::shared_ptr<const CERT_CONTEXT> certCertAuthorityRsaEVCA;
    auto tuple_result = GetCertificateFromFile(fileCertAuthorityRsaEVCA, &certCertAuthorityRsaEVCA);

    if (std::get<0>(tuple_result) != 0)
    
        std::cout << "Error: " << std::get<0>(tuple_result) << " " << std::get<1>(tuple_result) << " " << std::get<2>(tuple_result) << "\n";
        return std::get<0>(tuple_result);
    

    std::shared_ptr<const CERT_CONTEXT> certCertEV;
    std::vector<unsigned char> dataCertEV(signingCertContext->pbCertEncoded,
        signingCertContext->pbCertEncoded + signingCertContext->cbCertEncoded);
    tuple_result = GetCertificateFromMemory(dataCertEV, &certCertEV);

    if (std::get<0>(tuple_result) != 0)
    
        std::cout << "Error: " << std::get<0>(tuple_result) << " " << std::get<1>(tuple_result) << " " << std::get<2>(tuple_result) << "\n";
        return std::get<0>(tuple_result);
    

    // Issuer:  'Microsoft Code Verification Root'
    // Subject: 'CertAuthority RSA Certification Authority'
    auto fileCertCross = CertAuthority_ROOT;
    std::shared_ptr<const CERT_CONTEXT> certCertCross;
    tuple_result = GetCertificateFromFile(fileCertCross, &certCertCross);

    if (std::get<0>(tuple_result) != 0)
    
        std::cout << "Error: " << std::get<0>(tuple_result) << " " << std::get<1>(tuple_result) << " " << std::get<2>(tuple_result) << "\n";
        return std::get<0>(tuple_result);
    

    //certificate 1 Issuer  : '<Certificate Provider> RSA Certification Authority'
    //              Subject : '<Certificate Provider> Extended Validation Code Signing CA'
    //
    //certificate 2 Issuer  : '<Certificate Provider> Extended Validation Code Signing CA'
    //              Subject : '<Your company / entity name>'
    //
    //certificate 3 Issuer  : 'Microsoft Code Verification Root'
    //              Subject : '<Certificate Provider> Certification Authority'

    std::vector<std::shared_ptr<const CERT_CONTEXT> > certs;
    certs.push_back(certCertAuthorityRsaEVCA);
    certs.push_back(certCertEV);
    certs.push_back(certCertCross);

    std::shared_ptr<void> resultStore;
    tuple_result = FormMemoryCertStore(certs, CERT_STORE_ADD_NEW, &resultStore);

    if (std::get<0>(tuple_result) != 0)
    
        std::cout << "Error: " << std::get<0>(tuple_result) << " " << std::get<1>(tuple_result) << " " << std::get<2>(tuple_result) << "\n";
        return std::get<0>(tuple_result);
    

    certStoreInfo.hCertStore = resultStore.get();
    //--------------------------------------------------------------------

    SIGNER_CERT cert = ;
    cert.cbSize = sizeof(SIGNER_CERT);
    cert.dwCertChoice = SIGNER_CERT_STORE;
    cert.pCertStoreInfo = &certStoreInfo;

    // The algidHash of the signature to be created must match the
    // hash algorithm used to create the app package
    SIGNER_SIGNATURE_INFO signatureInfo = ;
    signatureInfo.cbSize = sizeof(SIGNER_SIGNATURE_INFO);
    signatureInfo.algidHash = CALG_SHA_256;
    signatureInfo.dwAttrChoice = SIGNER_NO_ATTR;

    SIGNER_SIGN_EX2_PARAMS signerParams = ;
    signerParams.pSubjectInfo = &subjectInfo;
    signerParams.pSigningCert = &cert;
    signerParams.pSignatureInfo = &signatureInfo;
    signerParams.dwTimestampFlags = SIGNER_TIMESTAMP_RFC3161;
    signerParams.pszAlgorithmOid = szOID_NIST_sha256;
    //signerParams.dwTimestampFlags = SIGNER_TIMESTAMP_AUTHENTICODE;
    //signerParams.pszAlgorithmOid = NULL;
    signerParams.pwszTimestampURL = TIMESTAMPURL;

    APPX_SIP_CLIENT_DATA sipClientData = ;
    sipClientData.pSignerParams = &signerParams;
    signerParams.pSipData = &sipClientData;

    // Type definition for invoking SignerSignEx2 via GetProcAddress
    typedef HRESULT(WINAPI *SignerSignEx2Function)(
        DWORD,
        PSIGNER_SUBJECT_INFO,
        PSIGNER_CERT,
        PSIGNER_SIGNATURE_INFO,
        PSIGNER_PROVIDER_INFO,
        DWORD,
        PCSTR,
        PCWSTR,
        PCRYPT_ATTRIBUTES,
        PVOID,
        PSIGNER_CONTEXT *,
        PVOID,
        PVOID);

    // Load the SignerSignEx2 function from MSSign32.dll
    HMODULE msSignModule = LoadLibraryEx(
        L"MSSign32.dll",
        NULL,
        LOAD_LIBRARY_SEARCH_SYSTEM32);

    if (msSignModule)
    
        SignerSignEx2Function SignerSignEx2 = reinterpret_cast<SignerSignEx2Function>(
            GetProcAddress(msSignModule, "SignerSignEx2"));
        if (SignerSignEx2)
        
            hr = SignerSignEx2(
                signerParams.dwFlags,
                signerParams.pSubjectInfo,
                signerParams.pSigningCert,
                signerParams.pSignatureInfo,
                signerParams.pProviderInfo,
                signerParams.dwTimestampFlags,
                signerParams.pszAlgorithmOid,
                signerParams.pwszTimestampURL,
                signerParams.pCryptAttrs,
                signerParams.pSipData,
                signerParams.pSignerContext,
                signerParams.pCryptoPolicy,
                signerParams.pReserved);
        
        else
        
            DWORD lastError = GetLastError();
            hr = HRESULT_FROM_WIN32(lastError);
        

        FreeLibrary(msSignModule);
    
    else
    
        DWORD lastError = GetLastError();
        hr = HRESULT_FROM_WIN32(lastError);
    

    // Free any state used during app package signing
    if (sipClientData.pAppxSipState)
    
        sipClientData.pAppxSipState->Release();
    

    return hr;

见this article I wrote。

【讨论】:

【参考方案12】:

我使用的是 globalsign 证书,他们也说了同样的话。

无法使用标准 EV 代码签名编写签名脚本,他们正在推广使用 HSM 平台

...这远远超出了我的预算。与他们所说的相反,我成功地使它起作用了:

"C:\Program Files (x86)\Microsoft SDKs\ClickOnce\SignTool\signtool.exe" sign /fd sha256 /f "MyCertificate.cer" /csp "eToken Base Cryptographic Provider" /kc "[TokenPassword]=ContainerTame" "FileToSign"

=> 此命令返回以下错误:

Error information: "CryptExportPublicKeyInfoEx failed" (87/0x57)

我真的不明白这个问题。 但是,如果您再次运行以下命令,它会起作用

"C:\Program Files (x86)\Microsoft SDKs\ClickOnce\SignTool\SignTool.exe" sign /tr http://timestamp.globalsign.com/scripts/timestamp.dll "MyFileToSign" 
Done Adding Additional Store
Successfully signed: MyFileToSign

这在 teamcity build 中有效,无需在 teamcity build 代理中登录活动帐户。

编辑:这个解决方案不再适用于我,globalsign 将时间戳 url 更改为 http://rfc3161timestamp.globalsign.com/advanced。因为那我不能再用 TokenPassword/ContainerName 签名了。我找到的唯一解决方案是无法单点登录并确保服务器不会注销(我在服务器上运行视频,因此我的帐户不会自动注销)。这是一个快速而肮脏的解决方案,但我找到的唯一一个。感谢 globalsign 的糟糕支持。

【讨论】:

以上是关于自动化扩展验证 (EV) 代码签名的主要内容,如果未能解决你的问题,请参考以下文章

每个 .exe 代码签名操作都需要 Digicert USB 令牌吗? (EV代码签名证书)

除了 Smartscreen 之外,代码签名 EV 证书的优势是啥?

EV 代码签名存在问题

Win10内核驱动强制签名,申请沃通 EV代码签名证书

我应该更喜欢哪一个,常规或 EV 代码签名证书?

天威诚信代码签名证书助手操作指南