badass-runtime-plugin icon indicating copy to clipboard operation
badass-runtime-plugin copied to clipboard

Sign exe before MSI installer is created

Open danielpeintner opened this issue 1 year ago • 5 comments

The task jpackageImage creates an application image. The task jpackage creates an MSI Installer of the application image (at least on Windows).

Requirement: I need to sign the executable which is wrapped in the MSI installer.

I can sign the MSI installer after the task jpackage is finished. However, virus scanner seem to need a signed executable in some cases as well. Hence I tried to first run jpackageImage , afterwards I signed the executable and after that I called jpackage to pack the result.

Unfortunately jpackage depends on jpackageImage and creates again a new application image which is not signed. How can I achieve that the already existing application image is used instead of creating a new one.

Thanks for any hint!

danielpeintner avatar Apr 13 '23 09:04 danielpeintner

I sign it within the jpackageImageTask (kotlin dsl):

/**
 * Extends the jpackageImage task.
 * For Windows, signs the exe file
 */
tasks.jpackageImage {
  when (osdetector.os) {
    Os.WINDOWS.osName -> {
      doLast {
        logger.info("Windows jpackageImage")
        //Add signing code here
      }
    }
    else -> {
      doLast {
        logger.info("Other OS")
      }
    }
  }
}

Note, you can ignore the osdetector stuff, I have that in because I also build for macOS.

Hopefully, that helps

vewert avatar Apr 13 '23 16:04 vewert

I'm adding this as enhancement because, although really it was just a question, similar plugins do just have a simple way to get things signed as they are built, and, in some cases, jpackage itself can be instructed to do signing.

Things to think about:

  • Assuming this will be implemented using signtool:
    • Which parameters are necessary?
    • Which optional parameters are worth adding?
    • How should the overall API look? (Try to fit with the way it will work for macOS - on macOS, jpackage just has additional flags. A future jpackage may gain the same for Windows for parity.)

hakanai avatar Apr 14 '23 05:04 hakanai

Thanks @vewert for your input 👍 It helped me to find a solution that worked for me.

I have an external bat script that I would like to start. Unfortunately with your solution I did not manage to start the script within tasks.jpackageImage (Note: I am a noob in Gradle). The solution I found is very similar. For the record I post it below...

def os = org.gradle.internal.os.OperatingSystem.current()

task runExecutableSign(type:Exec) {
    doFirst {
        println "Start Executable signing process ..."
        workingDir = file('./_sign/')
        commandLine = ['cmd', '/C', 'start', 'jsign-exe.bat']
        // cmd /C start D:/XXX/_sign/jsign-exe.bat
    }
}

if (os.windows) {
    jpackageImage.finalizedBy runExecutableSign
}

The jsign-exe.bat is very simple. The only issue I encountered was that the exe file was set to read-only. Hence you need to remove this flag and than sign it with your tools. Something like this

:: remove read-only flag
attrib -r "...foo.exe"

:: sign EXE
"%PROGRAMFILES(X86)%\Windows Kits\10\App Certification Kit\signtool.exe" sign /tr http://timestamp.digicert.com /td sha256 /fd sha256 /a  "...foo.exe"

Hope this might be useful for others.

  • How should the overall API look? (Try to fit with the way it will work for macOS - on macOS, jpackage just has additional flags. A future jpackage may gain the same for Windows for parity.)

@hakanai see above for options I need to sign..

danielpeintner avatar Apr 14 '23 07:04 danielpeintner

jpackage builds both an MSI and an EXE for Windows.

To sign the EXE I have this small task:

ext {
    signTool = "C:\\Program Files (x86)\\Windows Kits\\10\\App Certification Kit\\signtool.exe"
}

task signWindowsInstaller(type: Exec) {
    executable = "${signTool}"
    args = [
        'sign', '/v',
        '/d', project.name,
        '/f', "${signCertificate}",
        '/p', "${signPassword}",
        '/fd', 'SHA256',
        '/t', 'http://timestamp.digicert.com',
        "${buildDir}\\native\\${project.name}-${project.version}.exe",
        "${buildDir}\\native\\${project.name}-${project.version}.msi"
    ]
}

In order to sign the EXE within the MSI, I have overridden the package resource Post-image script, project-name-post-image.wsf. This is a custom script that is executed after the application image is created and before the MSI installer is built for both .msi and .exe packages.

https://docs.oracle.com/en/java/javase/16/jpackage/override-jpackage-resources.html#GUID-405708DC-0243-49FC-84D9-B2A7F0A011A9

<?xml version="1.0" ?>
<job>
  <script language="VBScript">
Const WshRunning = 0
Const WshFinished = 1
Const WshFailed = 2

Set WshShell = CreateObject("WScript.Shell")

Dim AttribOffExec : Set AttribOffExec = WshShell.Exec("attrib -r @projectName@\@[email protected]")
While AttribOffExec.Status = WshRunning
    WScript.Sleep 50
Wend

signCommand = """@signTool@"" sign /v /a /d ""@projectName@"" /f ""@signCertificate@"" /p @signPassword@ /fd SHA256 /t http://timestamp.digicert.com @projectName@\@[email protected]"

Dim SignExec : Set SignExec = WshShell.Exec(signCommand)
While SignExec.Status = WshRunning
    WScript.Sleep 50
Wend

Dim AttribOnExec : Set AttribOnExec = WshShell.Exec("attrib +r @projectName@\@[email protected]")

Dim output
If SignExec.Status = WshFailed Then
    output = SignExec.StdErr.ReadAll
Else
    output = SignExec.StdOut.ReadAll
End If

Dim StdOut : Set StdOut = CreateObject("Scripting.FileSystemObject").GetStandardStream(1)
Stdout.Write output
  </script>
</job>

DJViking avatar Apr 14 '23 12:04 DJViking

I used to use signtool, but now I use the jsign plugin: https://ebourg.github.io/jsign/ It can be called from within the jpackageImage task.

I also noticed about the read-only problem (sorry I should have mentioned that problem).

Here is my more complete code, including jsign and removing read-only:

/**
 * Extends the jpackageImage task.
 * For Windows, signs the exe file
 */
tasks.jpackageImage {
  when (osdetector.os) {
    Os.WINDOWS.osName -> {
      doLast {
        logger.info("Windows jpackageImage")

        Paths.get(jpackageWinDir.absolutePath, "${project.name}.exe").toFile().setWritable(true)

        val jsign = project.extensions.getByName("jsign") as groovy.lang.Closure<*>
        jsign(
          "file" to Paths.get(jpackageWinDir.absolutePath, "${project.name}.exe").toString(),
          "name" to longName,
          "url" to projectUrl,
          "keystore" to pfxFile.absolutePath,
          "storepass" to pfxPass,
          "alg" to signingAlg,
          "tsaurl" to tsaUrl,
          "tsmode" to tsMode
        )
      }
    }
    else -> {
      doLast {
        logger.info("Other OS")
      }
    }
  }
}

vewert avatar Apr 14 '23 15:04 vewert