我们如何能够在不需要用户干预的情况下自动化这个过程,并将密码存储/缓存到某个地方?
在这个帖子中进一步扩展已有的答案,可以使用微软的标准signtool程序来提供令牌密码。
0. 在高级视图中打开SafeNet客户端
安装路径可能会有所不同,但对于我来说,SafeNet客户端安装在:C:\Program Files\SafeNet\Authentication\SAC\x64\SACTools.exe
4. 将所有内容格式化在一起
eToken CSP具有隐藏(或至少没有广泛宣传的)功能,可以从容器名称中解析令牌密码。
格式有以下四个选项之一:
[]=name
[reader]=name
[{{password}}]=name
[reader{{password}}]=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 sign /f mycert.cer /csp "eToken Base Cryptographic Provider" /k "[{{TokenPasswordHere}}]=KeyContainerNameHere" myfile.exe
/c/PROGRA~2/WI3CF2~1/10/bin/10.0.19041.0/x64/signtool sign -f "certfile.cer" -csp "eToken Base Cryptographic Provider" -k "[{{Password*Goes$Here}}]=te-UUID" file.exe
- SRG3006据我所知,无法绕过登录对话框,但您可以配置SafeNet认证客户端,使其仅在每个登录会话期间询问一次。
我引用了SAC文档(一旦安装在 \ ProgramFiles \ SafeNet \ Authentication \ SAC \ SACHelp.chm
中找到,章节为“ 客户端设置”,“启用客户端登录”):
启用单一登录后,用户可以在每个计算机会话期间仅使用一个令牌密码访问多个应用程序。 这消除了用户需要单独登录到每个应用程序的需要。
要启用此默认情况下禁用的功能,请转到SAC高级设置并选中“启用单一登录”框:
重新启动计算机,现在它应该只提示一次令牌密码。 在我们的情况下,每次构建我们有200多个二进制文件要签名,因此这是绝对必要的。
否则,这是一个小的C#控制台示例代码(与m1st0等效),允许您自动响应登录对话框(可能需要以管理员身份运行)(您需要从控制台项目引用 UIAutomationClient.dll
和 UIAutomationTypes.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();
}
}
}
AutomationElement
树中没有显示)。我不知道是否可以通过某种方式解决它。但是重新访问这个问题并找到@Austin的答案后,我相信这是更好的解决方案。 - Martin Prikryl扩展此答案,可以使用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;
}
#include
之后插入#pragma comment(lib, "Cryptui.lib")
即可。 - zett42token_logon()
函数即可。之后,标准的 signtool.exe 就可以成功执行,而无需再次要求输入令牌密码。 - zett42signtool.exe sign /fd sha256 /f "signing.cer" /csp "eToken Base Cryptographic Provider" /kc "[{{在此输入令牌密码}}]=容器名称" "ConsoleApp1.exe"
请使用Microsoft Windows SDK 10来运行signtool。
我制作了一款Beta工具,可以帮助自动化构建过程。
这是一个客户端-服务器Windows应用程序。您可以在插入EV令牌的计算机上启动服务器。在服务器端应用程序启动时输入令牌密码后,您可以远程签署文件。 客户端应用程序完全替代了signtool.exe,因此您可以使用现有的构建脚本。
源代码位于此处:https://github.com/SirAlex/RemoteSignTool
编辑:我们在过去半年里成功地将该工具用于代码签名,而且在我们的构建服务器上24x7地运行,一切正常。
实际上,在Windows上,您可以完全以编程方式指定令牌密码。这可以通过使用形式为“\\。 \ AKS ifdh 0”的标记名称或标记容器名称(在身份验证客户端应用程序中的证书属性中可见的某个GUID)创建带有CRYPT_SILENT标志的上下文(CryptAcquireContext)来完成。然后,您需要使用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;
}
}
我使用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}
}
}
安装 https://chocolatey.org/docs/installation(可以使用管理员命令提示符中的一个命令完成)
(重新启动命令提示符)
抑制choco在每次安装时不停提示:
choco feature enable -n=allowGlobalConfirmation
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身份验证客户端
- 配置 > 客户端设置
> 高级
> 启用单点登录
选项可使密码提示数量最小化,但并不能完全禁用它们(在版本10.4.26.0上测试过)。
C# 应用程序(例如:https://github.com/ganl/safenetpass)在锁屏状态下无法使用,但可以使用此 Python 脚本。
我从Digicert得到了一个回答:
不幸的是,EV代码签名证书的一部分安全措施是每次都必须输入密码。无法自动化。
在我的情况下,Digicert为CI发放了标准(OV)证书,如果您已经拥有EV证书,则免费提供。
我知道这不是解决方案,但如果您无法将令牌放入服务器(云服务器),那么这就是走的方式。
signtool.exe
。如果使用过时的版本(我的版本是2016年的),则会出现错误信息:“CryptExportPublicKeyInfoEx failed”(87/0x57)。您可以通过安装Windows SDK来获取最新版本。至少在撰写本文时,SDK提供的版本支持使用Austin Morton所描述的方法。 - Crown