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

dec*_*jau 60 passwords code-signing authenticode code-signing-certificate

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

如何在没有用户干预的情况下通过在某处存储/缓存密码来自动执行此过程?

Sim*_*ier 54

没有办法绕过登录对话框AFAIK,但您可以做的是配置SafeNet身份验证客户端,以便每次登录会话只需要一次.

我引用了SAC文档(在安装时发现\ProgramFiles\SafeNet\Authentication\SAC\SACHelp.chm,章节' Client Settings',' Enabling Client Logon'):

启用单一登录后,用户可以访问多个应用程序,每次计算机会话期间只有一个令牌密码请求.这减轻了用户单独登录每个应用程序的需要.

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

在此输入图像描述

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

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

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();
      }
   }
}
Run Code Online (Sandbox Code Playgroud)

  • 这可能不是DigiCert的正式答案,但他们的答案很糟糕,这个很棒!谢谢您的帮助! (8认同)
  • 我可以确认这可以从TeamCity工作(只要TeamCity Windows服务具有"允许服务与桌面交互"复选框).我们还需要在另一个线程中运行密码输入过程,并在我们的构建机器上禁用"交互式服务检测"服务.我们在signtool周围创建了一个C#包装器,它执行签名并处理上面的密码输入,所有这些都在一个自包含的应用程序中.我无法相信我们必须跨越多少障碍才能实现这一目标,但对于同一船上的其他任何人,请专注于上述的C#方法...... (3认同)
  • +1表示正确答案.令人惊讶的是,看到人们开发脚本来自动化用户输入等等,真正打破密码的目的,他们需要知道的就是这个选项的位置.我怀疑这个选项是否会消失,因为发行人理解开发人员每次签署二进制文件时都不能输入密码. (2认同)
  • 仅供参考……赛门铁克 EV 证书也使用 SafeNet。我们必须围绕此过程构建一个笨拙的解决方案,但在阅读您的答案并实施控制台应用程序后,这极大地帮助了我们的构建过程。谢谢你。架构不佳的代码签名过程的绝佳解决方案。 (2认同)

小智 14

扩展这个答案,可以使用CryptAcquireContextCryptSetProvParam以编程方式输入令牌PIN和CryptUIWizDigitalSign以编程方式执行签名.我创建了一个控制台应用程序(下面的代码),它将证书文件作为输入(通过右键单击SafeNet Authentication Client中的证书并选择"导出..."),私钥容器名称(在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;
}
Run Code Online (Sandbox Code Playgroud)

将证书导出到文件:
将证书导出到文件

私钥容器名称:
私钥容器名称

  • 这个应该是接受的答案,它就像一个魅力! (2认同)
  • 太棒了。特别是在我意识到我只需要使用这个工具签署一些虚拟文件之后。如果启用了“单一登录”(SafeNet 驱动程序),则所有后续步骤均使用标准签名工具。这对于签署 Office Addins (VSTO) 非常有用,它使用不同的工具,这也意味着我的构建脚本/过程只需要最少的更改。 (2认同)

Aus*_*ton 13

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

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

安装路径可能会有所不同,但是对我而言,SafeNet客户端已安装为: C:\Program Files\SafeNet\Authentication\SAC\x64\SACTools.exe

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

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

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

3.查找您的读者名称 读者姓名

4.一起格式化

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

格式为以下之一

[]=name
[reader]=name
[{{password}}]=name
[reader{{password}}]=name
Run Code Online (Sandbox Code Playgroud)

哪里:

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

如果您连接了多个阅读器,则大概必须指定阅读器名称-因为我只有一个阅读器,所以我无法确认。

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
Run Code Online (Sandbox Code Playgroud)

从此答案中获取的一些图像:https : //stackoverflow.com/a/47894907/5420193

  • 尝试使用此方法时,其他人是否收到“SignTool 错误:没有可用的私钥。”?`/c/PROGRA~2/WI3CF2~1/10/bin/10.0.19041.0/x64/signtool sign -f "certfile.cer" -csp "eToken 基础加密提供程序 r" -k "[{{密码*Goes$这里}}]=te-UUID" file.exe` (5认同)
  • 警告:如果您输入错误的密码,即使只是两次,此解决方案也可能会锁定您的硬件令牌!不知何故,在我使用无效密码执行命令一次后,密码重试剩余计数器从 15 减少到 3。“etokensign.exe”解决方案似乎工作正常,但在一次无效密码尝试后,剩余密码计数器应从 15 减少到 14。 (3认同)
  • 太棒了,这对我来说非常适合我使用 Sectigo 提供的 SafeNet USB 加密狗。 (3认同)
  • 效果很好,遗憾的是我在两天挖掘和实施自己的“signtool”后发现了这个:D thx (2认同)
  • 据我所知,这种语法没有在任何地方公开记录。我通过对 IDA Pro 中的驱动程序二进制文件进行逆向工程发现了此功能。 (2认同)
  • 这比在 GUI 提示中输入伪造密码的其他答案更好。除此之外,这甚至在非交互式 Windows 会话中也有效。其他答案[似乎不适用于最新的工具](/sf/ask/1254952681/#comment114014202_26126701)。 (2认同)

Ale*_*nov 10

我做了beta工具,它将有助于自动化构建过程.

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

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

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

  • 这种方法的安全性如何?这是否意味着任何可以使用 HTTP 连接到您的签名服务器的人都可以使用您的 EV 证书签署他们想要的任何二进制文件? (2认同)

Lak*_*mal 7

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 进行签名工具


avz*_*kin 6

实际上在Windows上,您可以完全以编程方式指定令牌密码.这可以通过使用形式为"\\.\ AKS ifdh 0"的令牌名称或令牌容器名称创建带有标志CRYPT_SILENT 的上下文(CryptAcquireContext)来完成,这是在Authentication Client应用程序中的证书属性中可见的一些guid.然后,您需要使用带参数PP_SIGNATURE_PIN的CryptSetProvParam来指定令牌密码.之后,该进程可以使用该令牌上的证书来签署文件.
注意:一旦你创建上下文似乎完全适用于当前进程,不需要将它传递给其他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;
    }
}
Run Code Online (Sandbox Code Playgroud)


小智 5

我使用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}
    }   
  } 
Run Code Online (Sandbox Code Playgroud)

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

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


小智 5

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

(重新启动命令提示符)

禁止 choco 每次安装时不断提示:

choco feature enable -n=allowGlobalConfirmation
Run Code Online (Sandbox Code Playgroud)

安装python,使用命令:

choco install python
Run Code Online (Sandbox Code Playgroud)

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

pip install pypiwin32
Run Code Online (Sandbox Code Playgroud)

将以下文本保存到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()
Run Code Online (Sandbox Code Playgroud)

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

python disableAutoprompt.py
Run Code Online (Sandbox Code Playgroud)

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

C# 应用程序(例如https://github.com/ganl/safenetpass)不适用于锁定屏幕,但适用于此 python 脚本。