128

We recently purchased a DigiCert EV code signing certificate. We are able to sign .exe files using signtool.exe. However, every time we sign a file, it prompts for the SafeNet eToken password.

How can we automate this process, without user intervention, by storing/caching the password somewhere?

StayOnTarget
  • 11,743
  • 10
  • 52
  • 81
decasteljau
  • 7,655
  • 10
  • 41
  • 58
  • The question "[How safe are the password prompts of the SafeNet eToken 5110 or similar cryptographic hardware tokens?](https://security.stackexchange.com/questions/159645/how-safe-are-the-password-prompts-of-the-safenet-etoken-5110-or-similar-cryptogr)" is somewhat related, if it ever gets an answer it should be of interest to those assessing whether to automate the password entry. As I'm at it, if someone who currently owns that or a similar token reads this, if you can try to "hack" it and answer that question it would be greatly appreciated :) – gbr May 18 '17 at 17:34
  • unfortunately the answer that worked for me and that gets the most vote appears at the end of the answers list, so don't lose your time and go directly to Simon Mourier answers https://stackoverflow.com/a/26126701/27194 – Patrick from NDepend team Oct 03 '19 at 05:49
  • 1
    Just a heads up before attempting any of these solutions. Hardware tokens have a "Token Password retries remaining" counter (can be checked in the SafeNet Authentication Client). When experimenting, make sure that it never reaches zero for obvious reasons. Otherwise your will probably be permanently locked out of your hardware token and you will have to order a new one! Learned this the hard way... – Sundae Oct 09 '19 at 11:50
  • The answer by Simon unfortunately no longer works (see [my comment to the answer](https://stackoverflow.com/q/17927895/850848#comment114014202_26126701)). And the answer by Austin not only works, but is imo better anyway. – Martin Prikryl Oct 27 '20 at 08:28
  • 2
    The [method described by Austin Morton](https://stackoverflow.com/a/54439759/6667272) works like a charm but it is very important to note that it requires an **up-to-date version** of the `signtool.exe`. With an out-of-date version (mine was from 2016) I got the error ``` Error information: "CryptExportPublicKeyInfoEx failed" (87/0x57) ``` You can get an up-to-date version by installing the [Windows SDK](https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk/). At least at the time of writing this, the version provided with the SDK supports using [method described by Austin Mor – Crown Apr 29 '21 at 14:45

13 Answers13

100

Expanding on answers already in this thread, it is possible to provide the token password using the standard signtool program from microsoft.

0. Open SafeNet Client in Advanced View

Install paths may vary, but for me the SafeNet client is installed to: C:\Program Files\SafeNet\Authentication\SAC\x64\SACTools.exe

Click the gear icon in the upper right to open "advanced view". SafeNet Advanced View

1. Export your public certificate to a file from the SafeNet Client Exporting the Certificate to a File

2. Find your private key container name
Private Key Container Name

3. Find your reader name Reader Name

4. Format it all together

The eToken CSP has hidden (or at least not widely advertised) functionality to parse the token password out of the container name.

The format is one of the following four options:

[]=name
[reader]=name
[{{password}}]=name
[reader{{password}}]=name

Where:

  • reader is the "Reader name" from the SafeNet Client UI
  • password is your token password
  • name is the "Container name" from the SafeNet Client UI

Presumably you must specify the reader name if you have more than one reader connected - as I only have one reader I cannot confirm this.

Note that the double curly braces ({{ and }}) are part of the syntax and must be included in the command line argument.

5. Pass the information to signtool

  • /f certfile.cer
  • /csp "eToken Base Cryptographic Provider"
  • /k "<value from step 4>"
  • any other signtool flags you require

Example signtool command as follows

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

Some Images taken from this answer: https://stackoverflow.com/a/47894907/5420193

Danilo Bargen
  • 18,626
  • 15
  • 91
  • 127
Austin Morton
  • 1,150
  • 1
  • 8
  • 6
  • 3
    Works great, sadly i found this after two days digging and implementing own "signtool" :D thx – Lukáš Koten Mar 13 '19 at 10:10
  • 5
    Warning: This solution may lock you out of your hardware token if you enter the wrong password, even just twice! Somehow the password retries remaining counter decreased from 15 to 3 after I executed the command just once with an invalid password. The "etokensign.exe" solution seems to work ok though, the remaining password counter decreased as it should from 15 to 14 after one invalid password attempt. – Sundae Oct 09 '19 at 11:37
  • 2
    Wonderful, this worked perfectly for me with the SafeNet USB dongle that came to me from Sectigo. – JacobJ Dec 09 '19 at 23:50
  • This works perfectly - just curious as to how you found out about the required syntax? Is that documented anywhere? – David Mar 10 '20 at 16:09
  • 6
    As far as I know, this syntax is not publicly documented anywhere. I found this functionality by reverse engineering the driver binary in IDA Pro. – Austin Morton Mar 11 '20 at 17:04
  • It seems using the correct signtool is important. The one included with the windows 10 kit worked. None of the other signtool.exe i tried worked. – Kimmeh Sep 29 '20 at 08:27
  • 5
    This is better than the other answers that fake password inputs to GUI prompts. Among other, this works even in non-interactive Windows sessions. And the other answers [seem not to work with the latest tools](https://stackoverflow.com/q/17927895/850848#comment114014202_26126701). – Martin Prikryl Oct 22 '20 at 10:31
  • I can't make this work. Do I need quotes around the password or something? My command line (fudged) looks like this: `signtool sign /f myCert.cer /csp "eToken Base Cryptographic Provider" /k "[{{correctPassword}}]=d99!asdf1234" myMSI.msi` – Mark Wheeler Nov 07 '20 at 01:52
  • `d99!asdf1234` is your container name from step 2? And are you leaving the `{{}}` around the correct password? – Austin Morton Nov 08 '20 at 06:18
  • ist it `/k` or `/kc` i often see the `/kc` command? and also the official signtool documentation uses `/kc`? – Christian Schmitt Feb 01 '21 at 16:21
  • `/kc` does appear to be the correct flag according to the latest microsoft docs - but I can confirm that `/k` works with the signtool shipped as part of Visual Studio 2017 – Austin Morton Feb 02 '21 at 17:19
  • If you need to sign more files you can replace `myfile.exe` with `*.exe` to sign all exe files in folder or you can pass several individual filenames at the end like so: `myfile1.exe` `myfile2.exe` `myfile3.exe` and so on… – miran80 Mar 15 '21 at 18:45
  • 1
    This works amazingly! I just had to update `signtool.exe` from a more recent Windows SDK. This works as a Windows Service working in the background (serving network signing requests from build agents), no user interaction required. Works when the server is logged off and after reboot! – Oren Chapo Apr 28 '21 at 21:02
  • 5
    Does anyone else receive `SignTool Error: No private key is available.` when trying to use this method? `/c/PROGRA~2/WI3CF2~1/10/bin/10.0.19041.0/x64/signtool sign -f "certfile.cer" -csp "eToken Base Cryptographic Provide r" -k "[{{Password*Goes$Here}}]=te-UUID" file.exe` – SRG3006 Apr 30 '21 at 15:44
  • 1
    To follow up - the [signtool docs](https://learn.microsoft.com/en-us/dotnet/framework/tools/signtool-exe) state that with no private key and using `-f`, to use `-csp` and `-kc` as an alternative. I've tested the above^ with `-kc` as well with no luck. – SRG3006 Apr 30 '21 at 15:56
  • 1
    I have the same "No private key is available" error message using this method. Did you guys found an alternative or a fix ? @SRG3006 – Tigzy Aug 03 '21 at 10:11
  • @Tigzy, unfortunately I can’t remember what I did to remedy the situation. I believe I used SignTool that came with the latest Windows 10 SDK? Separately, I moved over to [jsign](https://ebourg.github.io/jsign/) which has the ability to use hardware tokens! Would recommend it over `osslsigncode` to be honest! – SRG3006 Aug 17 '21 at 16:13
  • I also have the "No private key is available" error. I have tried the latest SignTool from Win 10 SDK. This only happens if I log in remotely, or try to sign from Bamboo. If I log into our build server locally it works. Anyone got a fix since? – Scaroth Sep 14 '21 at 16:02
  • Update: Being logged into our signing PC with TeamViewer when our script is running allows successful signing. This is probably enough for us as release builds are manually kicked off on our CI server. A developer can open a TeamViewer session as part of our release process. Regular VNC clients might work also. – Scaroth Sep 17 '21 at 17:47
  • This answer worked with the SignTool.exe (File version 10.0.22000.194) obtained from the Windows SDK. Also note that SignTool now mandates the digest algorithm (/fd) to be specified when signing. One can use either "/fd SHA256" or "/fd certHash" to use the same digest as in the certificate. – Santosh Dec 03 '21 at 09:01
  • This worked great.. only param I added was /t "http://timestamp.digicert.com" so that the signing timestamp was not empty. – jrob Dec 14 '21 at 22:32
  • This did not work for me! I got the "No private key is available" error and have been locked out of the dongle after a single failed attempt! – Jonas Due Vesterheden Apr 05 '22 at 07:54
  • @Sundae: Yeah, that's a good warning. My EV cert/dongle locked up after like 3 bad passwords. The question is how to unlock it? Please advise. – c00000fd Apr 10 '22 at 17:38
88

There is no way to bypass the login dialog AFAIK, but what you can do is configure the SafeNet Authentication Client so it only asks it once per login session.

I quote the SAC doc (found once installed in \ProgramFiles\SafeNet\Authentication\SAC\SACHelp.chm, chapter 'Client Settings', 'Enabling Client Logon') here:

When single logon is enabled, users can access multiple applications with only one request for the Token Password during each computer session. This alleviates the need for the user to log on to each application separately.

To enable this feature which is disabled by default, go to SAC advanced settings, and check the "enable single logon" box:

enter image description here

Restart your computer, and it should now only prompt for the token password once. In our case, we have more than 200 binaries to sign per each build, so this is a total must.

Otherwise, here is a small C# console sample code (equivalent to m1st0 one) that allows you to respond automatically to logon dialogs (probably needs to run as admin) (you need to reference from your console project (UIAutomationClient.dll and 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();
      }
   }
}
Martin Prikryl
  • 188,800
  • 56
  • 490
  • 992
Simon Mourier
  • 132,049
  • 21
  • 248
  • 298
  • 13
    This may not be an official answer from DigiCert, but their answer sucks, and this one is awesome! Thanks for the help! – lordjeb Jan 15 '15 at 18:21
  • 3
    +1 for a correct answer. It amazes me to see people developing scripts to automate user input and such, defeating the purpose of having a password really, and all they needed to know was where this option was. I doubt this option will ever disappear, as the issuers understand developers can't type in the password every single time a binary is signed. – dyasta Jan 30 '16 at 16:53
  • 4
    I can confirm that this works from TeamCity (as long as the TeamCity Windows service has the "Allow service to interact with desktop" box ticked). We also needed to run the password entry process in another thread and to disable the "Interactive Services Detection" service on our build machine. We created a C# wrapper around signtool that performed the signing and handled the password entry as above, all in a self contained app. I can't believe how many hurdles we had to cross to get this working, but for anyone else in the same boat, focus on the C# method described above... – Alan Spark Mar 22 '16 at 14:54
  • @AlanSpark Can you be more specific as to how and why you needed to run it in a separate thread? I managed to make the wrapper program and if I run it locally it works beautifully. However, when I add it to the nant script that's executed by TeamCity, I get a "SignTool Error: No certificates were found that met all the given criteria" error. Did you guys also run into this? – enriquein Jul 27 '16 at 19:22
  • @enriquein To be honest, I'm not sure why we ended up using a separate thread but I do remember being in the situation where it worked locally but not when invoked by TeamCity. If I recall correctly, it was hanging when invoking the pattern. Regarding your other point, we also got that error where the certificate was not found when called by TeamCity. The problem was that there are two certificate stores in Windows, one for the current user and one for the local machine. I think because TeamCity uses a built in "System" user account it requires the local machine certificate store... – Alan Spark Jul 29 '16 at 07:41
  • @enriquein... I think it was just a case of copying the certificate from the current user store to the local machine store in MMC. Sorry if this is a bit vague but it has been some time. I hope it gives you the gist. – Alan Spark Jul 29 '16 at 07:41
  • @AlanSpark Thank you very much. I really appreciate you taking the time to reply. I'll start investigating right away. – enriquein Jul 29 '16 at 13:36
  • 2
    FYI...the Symantec EV Certs use SafeNet as well. We had to built a janky solution around this process but after reading your answer and implementing the console app, this has helped out our build process immensely. Thank you. Great solution to a poorly architected code signing process. – Zoltan Oct 24 '16 at 20:01
  • I've tried to get the c# solution to work with teamcity with no luck. I just get Error information: "Error: SignerSign() failed." (-2147023673/0x800704c7) which is the equivalent of clicking cancel when the sign in window pops up. It works just fine when running it locally but as soon as I try to run it with teamcity or via remote pssession or psexec then it doesn't work. Any thoughts? – longlostbro Feb 07 '19 at 19:42
  • FWIW, the older version 4.x didn't work correctly for me, but the latest (v9.0) makes this solution work splendidly. I updated the answer with a link to the new software, at https://knowledge.digicert.com/generalinformation/INFO1982.html – mrm Oct 31 '19 at 21:28
  • Did not work for me - at least everything before "Otherwise, here is a small C#" text. But after that text it makes sense, but I've posted my own answer with hithub link to same project. – TarmoPikaro Feb 05 '20 at 16:13
  • Second part of answer does not work either. Token password prompts still appears, switched to python solution, see here: https://stackoverflow.com/a/43469857/2338477 – TarmoPikaro Feb 13 '20 at 12:10
  • 1
    I have been successfully using this useful solution for a while. But now when setting it up on new machine with Windows 10 Pro 2004 with SafeNet client 9.0.34 x64 for Windows 8 and up, it does not work anymore. Where's a new password prompt. It seems to be a Windows built-in one, instead of the custom SafeNet prompt like before. And the password box of the new prompt is not automatable (it's not exposed in `AutomationElement` tree). I do not know if it can be worked around somehow. But visiting this question again and finding the answer by @Austin, I believe it's a better solution anyway. – Martin Prikryl Oct 22 '20 at 09:21
  • This was working perfectly on Windows 10 for the last year or so, but it is no longer working as of February 2023 near as I can tell. The Windows 10 PIN Dialog box has returned regardless of the settings on the SafeNet Authentication Client. You will have enter your password a whole bunch of times to sign say for instance a Windows Application Packaging Project (which none of these examples using signtool cover) – sonofsmog Feb 16 '23 at 08:26
24

Expanding on this answer, this can be automated using CryptAcquireContext and CryptSetProvParam to enter the token PIN programmatically and CryptUIWizDigitalSign to perform the signing programmatically. I created a console app (code below) that takes as input the certificate file (exported by right clicking the certificate in SafeNet Authentication Client and selecting "Export..."), the private key container name (found in SafeNet Authentication Client), the token PIN, timestamp URL, and the path of the file to sign. This console app worked when called by the TeamCity build agent where the USB token was connected.

Example Usage:
etokensign.exe c:\CodeSigning.cert CONTAINER PIN http://timestamp.digicert.com C:\program.exe

Code:

#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;
}

Exporting the Certificate to a File:
Exporting the Certificate to a File

Private Key Container Name:
Private Key Container Name

IInspectable
  • 46,945
  • 8
  • 85
  • 181
draketb
  • 444
  • 4
  • 5
  • 2
    This one should be the accepted answer, it works like a charm! – shawn Mar 27 '18 at 04:29
  • 2
    This is perfect. In particular after I realized that I only need to sign some dummy file with this tool. If the "single logon" is enabled (SafeNet driver), all subsequent steps work with the standard signtool. This is very useful for signing Office Addins (VSTO), which use a different tool and it also meant that only minimal changes were required for my build script / process. – Stefan Egli Oct 25 '18 at 05:54
  • This answer is a good addition to the one provided by avzhatkin. At this point the code is close to replacing signtools.exe. This program would need to support cross signing. Luckily there is another SO post with now to perform [cross signing](https://stackoverflow.com/questions/32210675/how-to-sign-an-exe-with-additional-certificates-using-cryptoapi-and-signersign?rq=1). – sdc Dec 06 '18 at 14:46
  • This answer ended up helping me the most. I missed an external reference when building in VS2017, but when adding some pragma comments as suggested [here](https://www.panagenda.com/2018/09/ev-code-signing-with-ci-cd/) I managed to get Bamboo (CI/CD by Atlassian) to sign. – HTBR Mar 07 '19 at 14:16
  • 1
    Link to "Cryptui.lib" to build successfully. For Visual C++ it is sufficient to insert `#pragma comment(lib, "Cryptui.lib")` after the `#include`s. – zett42 Feb 19 '21 at 17:19
  • @StefanEgli Actually, you don't even need to sign a dummy file. Only call to `token_logon()` function is necessary. After that, standard signtool.exe succeeds without asking for token password again. – zett42 Feb 19 '21 at 17:58
12

I'm made beta tool which will help to automate build process.

It's Client-Server windows application. You can start server on computer where EV token inserted. Enter password for token on server-side application startup. After this you can sign files remotely. Client side application fully replaces signtool.exe so you can use existing build scripts.

Source code located here: https://github.com/SirAlex/RemoteSignTool

Edit: We successfully used this tool for code signing last half-year 24x7 on our Build server. All works fine.

  • 2
    How secure is this approach? Doesn't it mean that anyone who can connect to your signing server with HTTP, can sign any binary they want with your EV certificate? – Gene Pavlovsky May 21 '19 at 09:51
12

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

Use Microsoft Windows SDK 10 for signtool

Lakmal
  • 779
  • 1
  • 8
  • 16
  • 1
    Superb! This one-liner certainly puts all the other answers to shame (although at first I was puzzled how to figure out my "Container name", until I found the instructions in draketb's answer above). – Dan Z Jun 05 '20 at 09:55
7

Actually on Windows you can specify the token password fully programmatically. This can be done by creating a context (CryptAcquireContext) with flag CRYPT_SILENT using token name in form "\\.\AKS ifdh 0" or token container name, which is some guid visible in cerificate properties in the Authentication Client application. You then need to use CryptSetProvParam with parameter PP_SIGNATURE_PIN to specify your token password. After that the process can use certificates on that token to sign files.
Note: once you create the context is seems to just work for current process entirely, no need to pass it to other Crypto API functions or anything. But feel free to comment if you find a situation when some more efforts will be required.
Edit: added code sample

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;
      }
}
Chris
  • 2,166
  • 1
  • 24
  • 37
avzhatkin
  • 136
  • 1
  • 4
  • 1
    Interesting. Seems promising, you should IMHO elaborate on that (enhance explanation, provide code, etc.) – Simon Mourier Sep 12 '17 at 14:47
  • Please post a full example. This sounds really useful – dten Sep 13 '17 at 09:59
  • thanks for the extra details. is this the guide you mention ?http://read.pudn.com/downloads128/ebook/549477/eToken_SDK_3_50[1].pdf – dten Oct 01 '17 at 12:29
  • I believe it is not the exact version I had, but it does seem to contain similar information about creating context and providing the PIN, though for different use scenario. – avzhatkin Oct 03 '17 at 12:04
  • I guess you call this function OpenToken(L"\\\\.\\AKS ifdh 0",)... well it worked for me! – Michael Haephrati Aug 21 '18 at 18:32
  • Good Solution, I have been using this for over a year now. I have a PS function that first calls a unmanaged exe with this code to unlock the token. Then from the PS file I call SignTool.exe. I have tried several methods to drop the unmanaged exe such as using a managed exe, embedding the C# source in the PS file, and writing a cmdlet which unlocked the token and then called SignTool from C#. None of these approaches worked. The token container was always unlocked in another process context. – sdc Dec 06 '18 at 14:40
5

I used AutoHotKey to automate the password entry using the script following. We have been trying to make a Web based front end for our developers to send the binaries to the Windows box with this script running so that it can be signed and returned.

  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}
    }   
  } 

I must note that what I shared is not completely not secure, but we also hit this issue requiring either purchasing signing keys for each developer or assigning a job of a signing manager that would approve the signature of released software. I believe those are the better, secure processes--that once things pass quality assurance and are approved for release, they can be officially signed. However, smaller company needs may dictate that this be done in some other automated way.

I originally used osslsigncode on Linux (before EV certificates) to automate signing of Windows executables (since we had a Linux server doing a lot of work for developer ease and collaboration). I have contacted the developer of osslsigncode to see if he can make use of the DigiCert SafeNet tokens to help automate it in a different way since I can see them on Linux. His reply provided hope but I am unsure of any progress and I could not dedicate more time to help

m1st0
  • 129
  • 2
  • 10
  • See the other answer. There is an option to unlock only once per session, which suffices for most users. – dyasta Jan 30 '16 at 16:54
4

Install https://chocolatey.org/docs/installation (Can be done using one command from administrative command prompt)

(Restart command prompt)

Supress choco's constant prompting for each installation:

choco feature enable -n=allowGlobalConfirmation

Install python, using command:

choco install python

(Restart command prompt) Install additional python module:

pip install pypiwin32

Save following text into 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()

Save your password into passwd.txt, and after that run

python disableAutoprompt.py

From SafeNet Authentication Client - configuration > Client Settings > Advanced > Enable Single Log On option can be enabled to minimize amount of password prompts, but it does not fully disable them (Tested on version 10.4.26.0)

C# application (E.g. https://github.com/ganl/safenetpass) does not work with lock screen on, but does work with this python script.

TarmoPikaro
  • 4,723
  • 2
  • 50
  • 62
A.N.
  • 278
  • 2
  • 13
  • This script is awesome, and I was able to easily adapt it to my needs working with a Yubikey dongle. However, Windows 10 breaks it. Windows 10 changes to XAML and so the win32gui.xxxx() functions won't work. /SIGH. Thanks Microsoft. This is why we can't have nice things. – John Rocha Feb 21 '20 at 17:57
3

Got an answer from Digicert:

Unfortunately, part of the security with the EV Code Signing Certificate is that you must enter the password everytime. There is not a way to automate it.

decasteljau
  • 7,655
  • 10
  • 41
  • 58
  • We got the same response, although they are looking into a solution they have no timeframe for when one might be available. They are aware of this SO post though so hopefully they will realise how much of an issue it is. – Alan Spark Mar 22 '16 at 14:58
  • We found a way around it: https://github.com/mareklinka/SafeNetTokenSigner/issues/8 – gunslingor Oct 15 '19 at 14:28
2

I my case Digicert issue an Standard (OV) certificate for the CI, for free if you already have an EV certificate.

I know this is not the solution but if you cant put the token in the server (a cloud server) this is the way to go.

Ricardo Polo Jaramillo
  • 12,110
  • 13
  • 58
  • 83
1

The way I do it is :

  1. Open the token

    PCCERT_CONTEXT cert = OpenToken(SAFENET_TOKEN, EV_PASS);

  2. Sign the file using the token, root / cross certificates when required and the EV certificate loaded into memory.

    HRESULT hr = SignAppxPackage(cert, FILETOSIGN);

Using SignerSignEx2():

The file is signed using SignerSignEx2() which needs to be loaded into memory using LoadLibrary() and 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();
}

Time Stamping

Further, you must Time Stamp your signed file and do that using a Time Stamping authority to which you connect.

That is done by securely checking a Time Stamping server via URL for the current date and time. Each Signing authority have their own Time Stamping server. Time Stamping is an extra step in the Code Signing process, but when it comes to EV Code Signing it is a requirement which adds an additional layer of security to the signed PE. For that reason, add to your code a check whether the user is connected to the Internet.

DWORD dwReturnedFlag;
if (InternetGetConnectedState(&dwReturnedFlag,0) == NULL) // use https://learn.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;
}

Loading a certificate from a file

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);
}

Loading a certificate into memory

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, "");
}

After the certificate has been loaded following accessing the hardware token we load it:

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

Finally, the signing is done in the following function:

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;
}

See this article I wrote.

Michael Haephrati
  • 3,660
  • 1
  • 33
  • 56
1

I am using a globalsign certificate and they nicely said the same thing.

It is not possible to script the signature with a standard EV code signing, they are promoting the use of an HSM platform

...which is far beyond my budget. Contrary what they said, I succeeded to make it work :

"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"

=> this command returns the following error :

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

I dont really understand this issue. BUT if you run another time the following command it works

"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

This works within teamcity build, and no need for an active account log in in teamcity build agent.

Edit : this solution does not work anymore for me, globalsign changed the timestamp url to http://rfc3161timestamp.globalsign.com/advanced. Since that i cant sign with TokenPassword/ContainerName anymore. The only solution I found is to unable single logon and make sure the server does not logoff (i run a video on the server so my account is not loggued off automatically). this is a quick and dirty solution but the only one I found. Thank you globalsign for your poor support.

0

We were able to automate both Windows EV code signing and Apple Notarization on a single Mac mini with install4j.

  • no PIN dialog popup
  • Sectigo EV Code Signing Certificate
  • SafeNet eToken 5110

If install4j can do it, there must be a way for your technology stack as well.

Reto Höhener
  • 5,419
  • 4
  • 39
  • 79