Maven Plugin POST but with Gradle

It’s real hacky, but here’s a power shell script that we use to for self-signing modules with the Gradle plugin. It uses PS to generate a self signed cert, then Java Keytool to create the keystore containing the cert, and then finally creates a gradle.properties file containing all the information needed for the plugin.

This could all be done in a platform independent way in a selfSignModule Gradle task, but I’m not sure about the consequences of making it so easy to produce self-signed modules with customizable DNS names.

$CertStoreLocation = "cert:\CurrentUser\My"
$JavaHome = 'C:\Program Files\Java\jdk-18.0.1\bin'
$FolderName = "signature"

$FolderPath = "$PSScriptRoot\$FolderName"
New-Item $FolderPath -ItemType Directory

$InfoFile = ($FolderPath + "\keystoreinfo.txt")
$GradleProps = ($FolderPath + "\gradle.properties")

$DnsName = Read-Host "Certificate DNS Name"
$YearsBeforeExp = Read-Host "Years before expiration"

Add-Type -AssemblyName System.Web
$CertPassword = [System.Web.Security.Membership]::GeneratePassword(32,4)
$CertPasswordSecure = ConvertTo-SecureString $CertPassword –asplaintext –force 
$KeystorePassword = [System.Web.Security.Membership]::GeneratePassword(32,4)
$KeystoreSecure = ConvertTo-SecureString $CertPassword –asplaintext –force 

$RootFileName = $DnsName.Replace(' ','_')
$RootFilePath = "$FolderPath\$RootFilePath"

$KeystoreFileName = ($RootFileName + "_keystore.jks")
$KeystoreFilePath = "$FolderPath\$KeystoreFileName"

Write-Output "Creating new certificate with DnsName '$DnsName'..."
$Cert = New-SelfSignedCertificate -DnsName $DnsName -Type CodeSigning -CertStoreLocation $CertStoreLocation -NotAfter (Get-Date).AddYears($YearsBeforeExp)

$FileName = "$RootFileName.p7b"
$FilePath = "$FolderPath\$FileName"
Write-Output "Exporting certificate to '$FileName'..."
$Results = Export-Certificate -Cert $Cert -Type P7B -FilePath "$FilePath"

$FileName = "$RootFileName.pfx"
$FilePath = "$FolderPath\$FileName"
Write-Output "Exporting certificate to '$FileName'..."
$Results = Export-PfxCertificate -Cert $Cert -FilePath "$FilePath" -Password $CertPasswordSecure

Write-Output "Removing certificate from personal store..."
$Cert | Remove-Item

$FileName = "$RootFileName.pfx"
Write-Output "Converting '$FileName' to '$KeystoreFileName'..."
$FilePath = "$FolderPath\$FileName"
$Results = Start-Process -FilePath "$JavaHome\keytool.exe" -ArgumentList "-importkeystore -srckeystore .\$FolderName\$FileName -srcstoretype pkcs12  -destkeystore .\$FolderName\$KeystoreFileName -deststoretype JKS -srcstorepass $CertPassword -storepass $KeystorePassword -keypass $CertPassword" -NoNewWindow -PassThru -Wait

Write-Output "Reading key aliases..."
$Results = Start-Process -FilePath "$JavaHome\keytool.exe" -ArgumentList "-list -v -keystore .\$FolderName\$KeystoreFileName -storepass $KeystorePassword" -NoNewWindow -PassThru -RedirectStandardOutput $InfoFile -Wait
$AliasInfo = @(Get-Content -Path $InfoFile | Where-Object { $_.Contains("Alias") })

$AliasName = "ALIAS NOT FOUND"
$line = $AliasInfo[0]
$start = $line.IndexOf(":") + 2
$length = $line.Length - $start
$Alias = $line.Substring($start, $length)
$AliasName = $Alias.Trim()
Write-Output "Alias '$AliasName' found."
#Start-Process -FilePath "$JavaHome\keytool.exe" -ArgumentList "-changealias -alias $Alias -destalias signing_cert -keypass $CertPassword -keystore $RootFileName.jks -storepass $KeystorePassword" -Wait



Write-Output "Generating gradle.properties..."
$content = @"
ignition.signing.certFile= /$FolderName/$RootFileName.p7b
ignition.signing.certPassword= $CertPassword
ignition.signing.certAlias= $AliasName
ignition.signing.keystoreFile= /$FolderName/$KeystoreFileName
ignition.signing.keystorePassword= $KeystorePassword
"@

Add-Content $GradleProps $content
1 Like

I think this will be great, that and the minimal perspective example that I know has been talked about on the forums a few times.

This solved my problem! I appreciate all of the help!

Now, in the vein of not knowing much about module signing, I am running into an issue trying to upload said freshly created/signed module.

Upon uploading I just receive an error that it failed to verify the module, and then the logs show this error:

com.inductiveautomation.ignition.gateway.modules.ModuleVerificationException: module verification failed
at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl.verifyModuleSignatures2(ModuleManagerImpl.java:1605)
at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl.loadModule(ModuleManagerImpl.java:1305)
at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl$2.call(ModuleManagerImpl.java:766)
at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl.executeModuleOperation(ModuleManagerImpl.java:949)
at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl.installModuleInternal(ModuleManagerImpl.java:739)
at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl$InstallCommand.execute(ModuleManagerImpl.java:1905)
at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl$Receiver.receiveCall(ModuleManagerImpl.java:1858)
at com.inductiveautomation.ignition.gateway.redundancy.QueueableMessageReceiver.receiveCall(QueueableMessageReceiver.java:47)
at com.inductiveautomation.ignition.gateway.redundancy.RedundancyManagerImpl.dispatchMessage(RedundancyManagerImpl.java:999)
at com.inductiveautomation.ignition.gateway.redundancy.RedundancyManagerImpl$ExecuteTask.run(RedundancyManagerImpl.java:1074)
at com.inductiveautomation.ignition.common.execution.impl.BasicExecutionEngine$ThrowableCatchingRunnable.run(BasicExecutionEngine.java:539)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at java.base/java.lang.Thread.run(Unknown Source)
Caused by: java.io.IOException: signature verification failed
at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl.verifyModuleSignatures2(ModuleManagerImpl.java:1600)
... 16 common frames omitted
Caused by: java.security.SignatureException: Signature length not correct: got 256 but was expecting 512
at java.base/sun.security.rsa.RSASignature.engineVerify(Unknown Source)
at java.base/java.security.Signature$Delegate.engineVerify(Unknown Source)
at java.base/java.security.Signature.verify(Unknown Source)
at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl.verifyModuleSignatures2(ModuleManagerImpl.java:1596)
... 16 common frames omitted

I then wanted to try doing it with the unsigned module, so I added the -Dignition.allowunsignedmodules=true flag to my conf, and then tried again. However I tried again with the signed module first, and it worked? Any ideas why I was required to add that flag for my signed module to upload?

Also, this works great to a gateway on localhost:8088, however is it possible to change the url and port with the current version of the plugin? Taking a look at the old plugin, it supports gatewayUrl and gatewayPort in ignitionModule, however I am not seeing anything like that in the current ModuleSettings.kt. Not sure if I am missing something, or it just isn't supported yet.

To answer my own question, I don't believe it was fully implemented. So I created a PR to do so. I made sure that it works on my side, however here's to hoping that I did it in the best way possible since I just started figured out gradle a couple days ago!

Honestly, not sure. Did you first install the module via the gateway once and accept the certificate? That's the only thing that immediately crosses my mind: the gateway won't load/run a signed module if it doesn't recognize the cert (either due to being one of our whitelisted first-party modules/certs, or because it has yet to be accepted). I think installing it once via the gateway UI and accepting the cert in that process once should satisfy that 'cert was accepted as ok' requirement.

The url and port are configurable as task inputs. If you run gradlew tasks you should get some info about the tasks and their flags. As task properties, that means you can override at the commandline:

gradlew deployModl --gateway https:://some.gateway.net:8099

If you wanted to use this configuration all the time, you could set the value via the tasks api in the buildscript:

// kotlinscript implementation example
import io.ia.sdk.gradle.modl.task.Deploy;

// down in the buildscript somewhere
tasks {
    withType<Deploy> {
        this.hostGateway.set("https://some.gateway.com:8099")
    }
}

We'll give this a look. I think @jcoffman has some updates he's going to be working on to ease the 'require signed module to build' aspect. I'll chat with him and we'll see if it makes sense to add the gateway as part of the plugin's configuration extension (aka - the ModuleSettings object).

1 Like

This part was actually not talking about using deployModl it was talking about manually uploading it directly. So this was while trying to do that

Interesting I wasn't aware you could override task properties in this way. If this is the ideal "official" way to do this, it would be good to be in the documentation as well when the plugin is next updated

Hmm, I'll have to look at this, not sure what's going on with that error. Adding the flag will get past it because we simply skip the module signature validation when allowing unsigned modules, but I'm not sure why you're hitting that to begin with. Might be useful to get a copy of the module if you're able to share (can send private message with a link, or email to pjones AT ia.io, or whatever mechanism works for you). Module doesn't need to be 'functional', just need to reproduce that error so I can try and figure out an explanation. Alternatively, I can repro how your cert was generated and try to reproduce it that way.

Ya, the task-level overrides are increasingly common. I think the shift to kotlin (type-aware) for buildscripts has really helped make that configuration style much easier to use, on account of the autocomplete you get now in supporting IDEs. Trying to configure tasks like that was a bit of a guessing game with the groovy based scripts.

I'll have to look and see if the Gradle team has settled on a 'best practice' for this sort of thing. I increasingly see the task-style configuration used, but not sure if there's a big win in that model vs using the Extension object API. May be that configuration speed is better with the task-level overrides (in particular with build-cache enabled), but would have to investigate.

Either way, I agree we can improve the README. Even if we add the property to the ModuleSettings object, the task options/flags/properties will still be configurable and supported in the same way I demo'd, since it's "just how Gradle works".

1 Like

This is super late, I must have worked around this originally when I first posted this with the unsigned modules flag. However I am now running into it in a production environment with the following module.

Would you be able to take a glance at it by chance? A copy of the module is included in the latest release.

I am not sure if its how I am signing it, or my cert specifically.

Here is a copy of the stacktrace:
com.inductiveautomation.ignition.gateway.modules.ModuleVerificationException: module verification failed
at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl.verifyModuleSignatures2(ModuleManagerImpl.java:1605)
at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl.loadModule(ModuleManagerImpl.java:1305)
at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl$2.call(ModuleManagerImpl.java:766)
at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl.executeModuleOperation(ModuleManagerImpl.java:949)
at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl.installModuleInternal(ModuleManagerImpl.java:739)
at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl$InstallCommand.execute(ModuleManagerImpl.java:1905)
at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl$Receiver.receiveCall(ModuleManagerImpl.java:1858)
at com.inductiveautomation.ignition.gateway.redundancy.QueueableMessageReceiver.receiveCall(QueueableMessageReceiver.java:47)
at com.inductiveautomation.ignition.gateway.redundancy.RedundancyManagerImpl.dispatchMessage(RedundancyManagerImpl.java:999)
at com.inductiveautomation.ignition.gateway.redundancy.RedundancyManagerImpl$ExecuteTask.run(RedundancyManagerImpl.java:1074)
at com.inductiveautomation.ignition.common.execution.impl.BasicExecutionEngine$ThrowableCatchingRunnable.run(BasicExecutionEngine.java:539)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at java.base/java.lang.Thread.run(Unknown Source)
Caused by: java.io.IOException: signature verification failed
at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl.verifyModuleSignatures2(ModuleManagerImpl.java:1600)
... 16 common frames omitted
Caused by: java.security.SignatureException: Signature length not correct: got 256 but was expecting 512
at java.base/sun.security.rsa.RSASignature.engineVerify(Unknown Source)
at java.base/java.security.Signature$Delegate.engineVerify(Unknown Source)
at java.base/java.security.Signature.verify(Unknown Source)
at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl.verifyModuleSignatures2(ModuleManagerImpl.java:1596)

Your cert public key is 512 bytes, but one or more of the files in your module is signed with a 256 bit/byte key?