diff --git a/.gitattributes b/.gitattributes index 7ce1194cb..15ec8e669 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,3 +4,4 @@ # Make.bat has always been stored with CRLF; the blanket eol=lf above would # silently rewrite the whole file the first time anything touches it. Make.bat -text +pkg/win32/*.bat text eol=crlf diff --git a/.github/workflows/check-windows-build.yml b/.github/workflows/check-windows-build.yml index ede36cd81..1dd0ccc26 100644 --- a/.github/workflows/check-windows-build.yml +++ b/.github/workflows/check-windows-build.yml @@ -1,10 +1,11 @@ # Replaces the pgadmin4-windows-x64-qa Jenkins job. # # This runs on a hosted runner, not a self-hosted one. Code signing in Make.bat -# is conditional on PGADMIN_WINDOWS_CSC being set (see Make.bat:393, :447 and -# :469), so an unsigned build check needs neither the Certum hardware token nor -# the machine it is plugged into. Only the release and snapshot builds, which -# do sign, need the self-hosted runner. +# is conditional on PGADMIN_WINDOWS_CSC being set (see :SIGN_FILES, +# :CREATE_INSTALLER and :VERIFY_SIGNATURE in Make.bat), so an unsigned build +# check needs neither the Certum hardware token nor the machine it is plugged +# into. Only the release and snapshot builds, which do sign, need the +# self-hosted runner. # # Dependencies come from dpage/winpgbuild, which is exactly where the # buildfarm's winpgbuild-update job gets them, so the toolchain matches what diff --git a/Make.bat b/Make.bat index 7299010ae..062d348ff 100644 --- a/Make.bat +++ b/Make.bat @@ -127,7 +127,7 @@ REM Main build sequence Ends IF "%PGADMIN_INNOTOOL_DIR%" == "" SET "PGADMIN_INNOTOOL_DIR=C:\Program Files (x86)\Inno Setup 6" IF "%PGADMIN_VCREDIST_DIR%" == "" SET "PGADMIN_VCREDIST_DIR=C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Redist\MSVC\14.40.33807" IF "%PGADMIN_VCREDIST_FILE%" == "" SET "PGADMIN_VCREDIST_FILE=vc_redist.x64.exe" - IF "%PGADMIN_SIGNTOOL_DIR%" == "" SET "PGADMIN_SIGNTOOL_DIR=C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64" + IF "%PGADMIN_SIGNTOOL_DIR%" == "" SET "PGADMIN_SIGNTOOL_DIR=C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64" IF "%PGADMIN_WINDOWS_CSC%" == "" SET "PGADMIN_WINDOWS_CSC=" REM Set additional variables we need @@ -444,8 +444,12 @@ REM Main build sequence Ends DEL /s "%WD%\pkg\win32\installer.iss.in_stage*" > nul ECHO Creating windows installer using INNO tool... + REM Inno signs the installer and its uninstaller itself, one file per call, + REM through the same script as :SIGN_FILES. Inno replaces $q with a quote + REM and $f with the already-quoted file name. The outer pair of quotes is + REM for cmd /c, which strips the first and last quote of its command line. IF NOT "%PGADMIN_WINDOWS_CSC%" == "" ( - CALL "%PGADMIN_INNOTOOL_DIR%\ISCC.exe" "%WD%\pkg\win32\installer.iss" "/DSIGNED" "/SpgAdminSigntool=%PGADMIN_SIGNTOOL_DIR%\signtool.exe sign /sm /n $q%PGADMIN_WINDOWS_CSC%$q /tr http://timestamp.digicert.com /td sha256 /fd sha1 /v $f" || EXIT /B 1 + CALL "%PGADMIN_INNOTOOL_DIR%\ISCC.exe" "%WD%\pkg\win32\installer.iss" "/DSIGNED" "/SpgAdminSigntool=%ComSpec% /c $q$q%WD%\pkg\win32\sign-files.bat$q $q%PGADMIN_WINDOWS_CSC%$q $f$q" || EXIT /B 1 ) ELSE ( CALL "%PGADMIN_INNOTOOL_DIR%\ISCC.exe" "%WD%\pkg\win32\installer.iss" || EXIT /B 1 ) @@ -489,22 +493,18 @@ REM Main build sequence Ends EXIT /B 0 -REM Sign one or more files, passed as quoted arguments. Signing is done in a -REM single signtool invocation as the hardware token prompts for a PIN on the -REM first signature of a session. -REM -REM The file digest is SHA-1 because that is all the build host's key can do: -REM the certificate is bound to the Certum card's legacy CryptoAPI provider, -REM crypto3 CSP, and asking it for a SHA-256 digest fails in SignerSign() with -REM 0xc0000225 before the PIN is even requested. Moving to SHA-256 means -REM rebinding the certificate to the CNG provider first; see pkg\win32\SIGNING.md. -REM The same flag appears in the Inno Setup signtool string in :CREATE_INSTALLER. +REM Sign one or more files, passed as quoted arguments, with a SHA-256 file +REM digest. pkg\win32\sign-files.bat does the work, in signtool's split +REM digest, sign and ingest steps, because the one-step form cannot use a +REM SHA-2 digest with the card's key; the script explains why. All the files +REM go in one call because each call costs one PIN prompt, and their names +REM must be unique, which the script checks. :SIGN_FILES IF "%PGADMIN_WINDOWS_CSC%" == "" EXIT /B 0 - CALL "%PGADMIN_SIGNTOOL_DIR%\signtool.exe" sign /sm /n "%PGADMIN_WINDOWS_CSC%" /tr http://timestamp.digicert.com /td sha256 /fd sha1 /v %* + CALL "%WD%\pkg\win32\sign-files.bat" "%PGADMIN_WINDOWS_CSC%" %* REM IF ERRORLEVEL, not IF %ERRORLEVEL%: the latter would be expanded before - REM signtool had run were this ever moved inside a parenthesised block. + REM the script had run were this ever moved inside a parenthesised block. IF ERRORLEVEL 1 ( ECHO. ECHO ************************************************************ diff --git a/pkg/win32/SIGNING.md b/pkg/win32/SIGNING.md index 6561cb852..81cc03627 100644 --- a/pkg/win32/SIGNING.md +++ b/pkg/win32/SIGNING.md @@ -191,38 +191,71 @@ update does not require editing anything. ## Certificate setup The card's "simple" registration through proCertum Card Manager does not -correctly link the certificate to its private key, and without the repair -below signing fails. +correctly link the certificate to its private key, and without the repair in +step 4 signing fails. **All of this needs local console access.** Smart card operations do not work over Remote Desktop, and the usual symptom is `certutil -key` failing with `NTE_KEYSET_NOT_DEF (0x80090019)`. +### 0. Before starting + +Take a VM snapshot, or a full backup if the host is physical. Install the ACS +CCID reader driver, proCertum Card Manager and the Windows SDK's Signing Tools; +the versions known to work are driver 1.0.6.1, Card Manager 4.14.0 and SDK +10.0.26100.0. Keep the Common profile PIN to hand, remembering that wrong PINs +count towards blocking the card. + ### 1. Put proCertum in CSP mode -Open proCertum Card Manager, go to Options, select **CSP driver** rather than -Minidriver, apply, and restart Windows. - -### 2. Import the certificate to the machine store - -In proCertum Card Manager: Read Card, then the Common Profile tab, select the -code signing certificate, Show certificate details, and install it into the -Windows **machine** store, under Personal. The machine store is why the build -signs with `/sm`. - -### 3. Link the certificate to the key - -`certutil -store My` will show the certificate with `No key provider -information`, which is expected at this point. - -Find the key container: +Open proCertum Card Manager, go to Options, set the driver for the Common +profile to **CSP** rather than Minidriver, leave "Enable PIN cache for +CSP-based applications" on, apply, and restart Windows. Then check: ```batch -certutil -key -csp "crypto3 CSP" +certutil -csplist | findstr /i "crypto3 Certum" ``` -Create `keyprov.inf`, substituting the container ID. The trailing `&` on each -line is required INF syntax, not a typo: +which should list `Provider Name: crypto3 CSP` with +`Provider Type: 1 - PROV_RSA_FULL`. + +### 2. Check the key is reachable + +```batch +certutil -csp "crypto3 CSP" -key +``` + +should list one container, marked `[Default Container]`, with `RSA` and +`AT_KEYEXCHANGE, AT_SIGNATURE`; note its ID for step 4. `NTE_KEYSET_NOT_DEF` +here means Remote Desktop, the wrong driver mode, no restart since changing +it, or a card that is not seated. + +The other Certum providers are no use for this key: `cryptoCertum3 KSP` only +sees the card's Secure profile, and the code-signing key is in the Common +profile, whilst `cryptoCertum3 CSP` fails with `NTE_BAD_PROVIDER`. + +### 3. Import the certificate to the machine store + +In proCertum Card Manager: Read Card, then the Common Profile tab, select the +code signing certificate, Show certificate details, and Install certificate +into the Windows **machine** store (Local Machine, Personal). The machine +store is why the build signs with `/sm`. + +`certutil -store My` will then show the certificate, issued by `Certum Code +Signing 2021 CA`, with its `Cert Hash(sha1)` thumbprint and `No key provider +information`, which is expected at this point. + +### 4. Link the certificate to the key + +As Administrator, first save the certificate's current properties, which are +what a rollback puts back: + +```batch +certutil -store -v My "" > before-repair.txt +``` + +Create `keyprov.inf`, substituting the container ID from step 2. The trailing +`&` on each line is required INF syntax, not a typo: ```ini [Properties] @@ -234,15 +267,55 @@ line is required INF syntax, not a typo: _continue_ = "KeySpec=2" ``` -Find the certificate's SHA1 thumbprint with `certutil -store My`, looking for -`Cert Hash(sha1)`, and repair the store as Administrator: +Then repair the store and check it: ```batch certutil -repairstore My "" keyprov.inf +certutil -store My "" ``` -`certutil -store My` should now report `Provider = crypto3 CSP` and a -`Key Container`. +The second command runs a signature test, so it raises one PIN prompt, and +should report the `Key Container`, `Provider = crypto3 CSP`, `Private key is +NOT exportable` and `Signature test passed`. If the test does not pass, check +the container ID, that the provider name is exactly `crypto3 CSP`, and +`KeySpec=2`. + +Only the certificate's store properties change here; the key on the card is +not touched. To undo it, rerun `-repairstore` with the values saved in +`before-repair.txt`, or restore the snapshot. + +### 5. Prove signing works + +On a throwaway copy of any unsigned executable, run the same steps the build +does (see below), at the console so that the PIN prompt can be answered by +hand if the helper is not yet running: + +```batch +powershell -NoProfile -Command "[IO.File]::WriteAllBytes('signer.cer', (Get-Item Cert:\LocalMachine\My\).Export('Cert'))" +mkdir dig +signtool sign /dg dig /fd sha256 /f signer.cer test.exe +signtool sign /ds /sm /n "" /fd sha256 dig\test.exe.dig +signtool sign /di dig test.exe +signtool timestamp /tr http://timestamp.digicert.com /td sha256 test.exe +signtool verify /pa /v test.exe +``` + +Only the `/ds` step prompts for the PIN. Each sign step should report +`Successfully signed`, the timestamp `Successfully timestamped`, and the +verification a `Hash of file (sha256)`, `The signature is timestamped` and +`Successfully verified`. + +The one-step form, `signtool sign /sm /n "" /fd sha256 +... test.exe`, fails after about a second, without asking for the PIN, with +`SignerSign() failed. (-1073741275/0xc0000225)`. That is expected, and is not +a setup fault; see below. + +### Undoing it all + +Restore the snapshot. Without one, set the proCertum driver back and restart, +then put the certificate's properties back as in step 4. Deleting and +reimporting the certificate in the store would probably also work, but whether +that can delete the key on the card was not tested, so prefer the snapshot. ## How the build signs @@ -253,36 +326,71 @@ certificate renewal is a settings change rather than a commit. The snapshot workflow fails immediately if it is unset, rather than quietly producing an unsigned installer that looks like a successful build. -The signing call it makes is equivalent to: +All signing goes through `pkg\win32\sign-files.bat`, called by `Make.bat` for +the components it signs, and by Inno Setup (through `cmd /c`) for the +installer and its uninstaller. For a set of files, it: -```batch -signtool sign /sm /n "" /tr http://timestamp.digicert.com /td sha256 /fd sha1 /v -``` +1. exports the certificate's public part from the machine store, requiring + exactly one certificate whose name is the subject, with a private key; +2. `signtool sign /dg /fd sha256 /f signer.cer ` writes each + file's SHA-256 digest into a private working directory, without the key; +3. `signtool sign /ds /sm /n "" /fd sha256 \.dig ...` + signs the digests with the card, which is the only step that uses it; +4. `signtool sign /di ` puts the signatures into the files; +5. `signtool timestamp /tr http://timestamp.digicert.com /td sha256 ` + adds the timestamp, which the split steps cannot do as part of signing; +6. `signtool verify /pa ` checks the result. -| Parameter | Why | -|---|---| -| `/sm` | machine store, which is where the certificate was installed | -| `/n` | selects the certificate by subject name | -| `/fd sha1` | file digest algorithm; see below for why it is not SHA-256 | -| `/tr` | RFC 3161 timestamp server; prefer this over the legacy `/t` | -| `/td sha256` | timestamp digest algorithm | +`/dg` names each digest after the file's name alone, so two files with the +same name in one call would overwrite each other's digest; the script refuses +such a call rather than sign one file with the other's signature. Everything +`Make.bat` signs in one call comes from the one directory, so this does not +arise in the build. -The file digest is SHA-1 because the key, bound through step 3 to the legacy -CryptoAPI provider `crypto3 CSP`, cannot produce anything else: `/fd sha256` -fails within a couple of seconds, before the PIN is requested, with -`Error: SignerSign() failed. (-1073741275/0xc0000225)`. The timestamp digest is -unaffected, since the card's key plays no part in the timestamp, which is why -`/td sha256` works. Getting to SHA-256 file digests means binding the certificate to -Certum's CNG provider, `cryptoCertum3 KSP`, instead. That is untested; before -switching the build over, sign a scratch copy of an executable with each digest -at the console, and snapshot the host before changing the binding. +Each `/ds` process asks for the PIN once, however many files it signs, and the +PIN is not cached between processes, so a build raises three prompts: one for +the components, and one each for Inno's installer and uninstaller. Each of +Inno's retries (`SignToolRetryCount`, 2 by default) is another prompt. The +helper answers them all. + +### Why not a single `signtool sign /fd sha256` + +That is the usual command, and it fails here, as step 5 above shows. The card, +the CSP and the binding are all capable of SHA-256: the CSP signs a SHA-256 +hash correctly through CryptoAPI, and the split steps work. The failure is in +crypt32's message-signing code, which signtool uses through `SignerSign()`. +For a SHA-2 digest it does not sign with a legacy CryptoAPI key directly, but +first converts the key handle to a CNG one with `NCryptTranslateHandle`, +which, as +[Microsoft documents](https://learn.microsoft.com/en-us/windows/win32/api/ncrypt/nf-ncrypt-ncrypttranslatehandle), +only works when a CNG key storage provider is registered under the CSP's name +or an alias of it. None is registered for `crypto3 CSP` (Certum's +`cryptoCertum3 KSP` aliases only `cryptoCertum3 CSP`), so the call fails with +`STATUS_NOT_FOUND`, 0xc0000225, before the card is used. SHA-1 takes the older +CryptoAPI path, which is why it worked. That `NCryptTranslateHandle` is the +failing call is inferred from it returning the same code when called directly, +rather than confirmed under a debugger, and why `/ds` avoids the conversion is +observed rather than documented. + +Binding the certificate to `cryptoCertum3 KSP` instead cannot work, as that +provider cannot see the code-signing key (step 2). + +Certum's own answer to SHA-2 problems with signtool, in their manual *Code +Signing: Using Signtool and Jarsigner*, is to switch proCertum to Minidriver +mode, which would make the one-step command work through Microsoft's Smart +Card Key Storage Provider. That is untested here. It changes the driver and the +certificate binding, so it needs this setup redone and a snapshot first, and +the PIN prompt would probably become a Windows Security dialog, which the +helper does not answer. + +### Timestamps and verification Timestamping matters: without it, signatures stop validating when the certificate expires. If DigiCert's server is unavailable, `http://timestamp.sectigo.com` and `http://ts.ssl.com` are alternatives. Certum's own, `http://time.certum.pl`, was unreliable during setup. -Verify with: +To check a signed file by hand: ```batch signtool verify /pa /v @@ -295,13 +403,21 @@ Remote Desktop: smart card operations need a local console session. Otherwise check proCertum is in CSP mode rather than Minidriver, that Windows was restarted after changing it, and that the card is seated and readable. -**"No certificates were found that met all the given criteria".** Check `/sm` -is being passed, that `certutil -store My` shows the certificate, and that the -key provider link from step 3 exists. +**"No certificates were found that met all the given criteria", or +`sign-files: expected exactly one certificate`.** Check that +`certutil -store My` shows the certificate, that `PGADMIN_WINDOWS_CSC` is its +subject name exactly, that no second certificate (an expired predecessor, say) +has the same name, and that the key provider link from step 4 exists. -**`SignerSign() failed`.** Check the card is inserted and readable, that the -PIN prompt has not appeared behind another window, and that the -certificate-to-key link is intact. +**`SignerSign() failed. (-1073741275/0xc0000225)`, with no PIN prompt.** +Something has run the one-step `signtool sign /fd sha256`, which cannot work +with this key; see 'Why not a single `signtool sign /fd sha256`'. Sign through +`sign-files.bat` instead. + +**Any other `SignerSign() failed`.** Check the card is inserted and readable, +that the PIN prompt has not appeared behind another window, and that the +certificate-to-key link is intact. A failed call leaves its working files in +the directory it names, under `%TEMP%`. **The build hangs rather than failing.** The runner is almost certainly running as a service, or as a scheduled task set to "run whether user is logged on or @@ -317,9 +433,11 @@ is wrong, which needs the card checking before it is retried enough times to block. Recovery is to log in to the console, clear whatever is on screen, restart the helper, and rerun the job. -**The first signature after the card is inserted prompts for a PIN.** Expected. -Subsequent operations in the same session may be cached, depending on proCertum -settings. +**Every signing call prompts for the PIN.** Expected: the PIN is asked for +once per process that uses the key, and is not cached between processes, so a +build raises one prompt for the components and one each for the installer and +the uninstaller. `certutil -store My` raises one too, because it runs a +signature test. ## Fully unattended signing diff --git a/pkg/win32/certum-pin-handler.ahk b/pkg/win32/certum-pin-handler.ahk index 96fc4df3b..439f49962 100644 --- a/pkg/win32/certum-pin-handler.ahk +++ b/pkg/win32/certum-pin-handler.ahk @@ -41,16 +41,20 @@ Persistent ; The proCertum software is localised, and the host runs it in Polish, so the ; dialog is titled "Logowanie do profilu zwyklego" ("log in to the common ; profile"), with a Polish barred l in the last word, rather than anything in -; English. Only the leading, unaccented part of that is matched, with +; English. Only the part of that before the barred l is matched, with ; SetTitleMatchMode 2 for a substring match: it survives the wording after it -; changing between proCertum releases or profiles, and it keeps this file to -; plain ASCII, so that no editor or AutoHotkey version can quietly mangle an -; accented character and leave behind a title that never matches. If the -; display language is ever changed, this is the line to change with it. +; changing between proCertum releases, and it keeps this file to plain ASCII, +; so that no editor or AutoHotkey version can quietly mangle an accented +; character and leave behind a title that never matches. The match stops no +; earlier than "zwyk" because a login to the card's Secure profile, which +; has a different PIN, would otherwise match too; nothing in the build uses +; that profile, but should anything ever ask, it must not be answered with +; this PIN. If the display language is ever changed, this is the line to +; change with it. SetTitleMatchMode 2 DetectHiddenWindows false -PIN_DIALOG := "Logowanie do profilu" +PIN_DIALOG := "Logowanie do profilu zwyk" PIN_DIR := EnvGet("LOCALAPPDATA") if (PIN_DIR = "") diff --git a/pkg/win32/sign-files.bat b/pkg/win32/sign-files.bat new file mode 100644 index 000000000..55920c96c --- /dev/null +++ b/pkg/win32/sign-files.bat @@ -0,0 +1,93 @@ +@ECHO OFF +REM Sign one or more files with a SHA-256 file digest: +REM +REM sign-files.bat "" "file" ["file" ...] +REM +REM Called by Make.bat for the components it signs, and by Inno Setup, through +REM cmd.exe, for the installer and its uninstaller. signtool.exe is taken from +REM PGADMIN_SIGNTOOL_DIR, which Make.bat sets. +REM +REM The one-step "signtool sign /fd sha256" cannot be used. The card's key is +REM reached through Certum's legacy CryptoAPI provider, crypto3 CSP, and for a +REM SHA-2 digest Windows' CMS code first converts that key handle to a CNG one +REM with NCryptTranslateHandle, which only works when a CNG provider is +REM registered under the same name. None is, so signing fails with 0xc0000225 +REM before the card is touched. signtool's split flow avoids that conversion: +REM /dg writes the digests without the key, /ds signs them on the card, /di +REM puts the signatures into the files, and the timestamp is added separately +REM because /tr cannot be combined with the split flow. See +REM pkg\win32\SIGNING.md. +REM +REM Only /ds uses the card, and it is one signtool process however many files +REM it signs, so a call to this script costs one PIN prompt. +REM +REM Paths containing ! are not supported, as delayed expansion would eat it. +SETLOCAL EnableDelayedExpansion + +SET "SIGNTOOL=%PGADMIN_SIGNTOOL_DIR%\signtool.exe" +IF NOT EXIST "%SIGNTOOL%" ( + ECHO sign-files: signtool.exe not found; is PGADMIN_SIGNTOOL_DIR set? + EXIT /B 1 +) + +SET "SUBJECT=%~1" +IF "!SUBJECT!" == "" ( + ECHO sign-files: no certificate subject given + EXIT /B 1 +) +SHIFT + +SET "FILES=" +:ARGS +IF "%~1" == "" GOTO ARGS_DONE +SET "FILES=!FILES! "%~f1"" +SHIFT +GOTO ARGS +:ARGS_DONE +IF "!FILES!" == "" ( + ECHO sign-files: no files given + EXIT /B 1 +) + +REM A private working directory per call, so that neither a concurrent run nor +REM one that failed and left its files behind can mix digests with this one. +SET "WORK=%TEMP%\sign-files-%RANDOM%%RANDOM%" +MKDIR "!WORK!" || EXIT /B 1 + +REM /dg names each digest after the file's name alone, so two files with the +REM same name would overwrite one another's digest and one would be signed +REM with the other's signature. Refuse rather than rely on the callers. +SET "DIGS=" +FOR %%f IN (!FILES!) DO ( + IF EXIST "!WORK!\%%~nxf.name" ( + ECHO sign-files: more than one file is named %%~nxf; sign them in separate calls + GOTO FAIL + ) + TYPE NUL > "!WORK!\%%~nxf.name" + SET "DIGS=!DIGS! "!WORK!\%%~nxf.dig"" +) + +REM /dg needs the signing certificate, but only its public part: exported from +REM the machine store here, since asking certutil for it runs a signature test +REM and so raises a PIN prompt. The match is on the certificate's simple name, +REM and exactly one must match, so a subject that signtool /n would take as a +REM substring of a longer name fails here rather than signing with the wrong +REM certificate. +powershell -NoProfile -NonInteractive -Command "$c = Get-ChildItem Cert:\LocalMachine\My | Where-Object { $_.GetNameInfo('SimpleName', $false) -eq $env:SUBJECT -and $_.HasPrivateKey }; if (@($c).Count -ne 1) { exit 1 }; [IO.File]::WriteAllBytes((Join-Path $env:WORK 'signer.cer'), $c.Export('Cert'))" +IF ERRORLEVEL 1 ( + ECHO sign-files: expected exactly one certificate named "!SUBJECT!" in the machine store + GOTO FAIL +) + +"%SIGNTOOL%" sign /dg "!WORK!" /fd sha256 /f "!WORK!\signer.cer" !FILES! || GOTO FAIL +"%SIGNTOOL%" sign /ds /sm /n "!SUBJECT!" /fd sha256 !DIGS! || GOTO FAIL +"%SIGNTOOL%" sign /di "!WORK!" !FILES! || GOTO FAIL +"%SIGNTOOL%" timestamp /tr http://timestamp.digicert.com /td sha256 !FILES! || GOTO FAIL +"%SIGNTOOL%" verify /pa !FILES! || GOTO FAIL + +RMDIR /S /Q "!WORK!" +EXIT /B 0 + +:FAIL +ECHO sign-files: signing failed; the working files are in !WORK! +EXIT /B 1