javagradlejavafxbouncycastlejlink

Digital signature JavaFX application throwing java.lang.NoClassDefFoundError: org/bouncycastle/cert/X509CertificateHolder when executing image


I'm having problems with the BouncyCastle library when trying to digitally sign a document with an application made with JavaFX. The error ONLY shows up when I'm doing the signing using a compiled image of the application. If I run the application through my IDE (Intellij Idea), it works perfectly fine. Maybe something's happening when I'm compiling the image that doesn't pull up the correct dependencies?

Here are the relevant pieces of code:

Dependencies in build.gradle:

dependencies {
  implementation('org.controlsfx:controlsfx:11.2.1')
  implementation('org.kordamp.bootstrapfx:bootstrapfx-core:0.4.0')

  testImplementation("org.junit.jupiter:junit-jupiter-api:${junitVersion}")
  testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${junitVersion}")

  // https://mvnrepository.com/artifact/com.itextpdf/itextpdf
  implementation('com.itextpdf:itextpdf:5.5.13.3')

  // https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk15to18
  implementation('org.bouncycastle:bcprov-jdk15to18:1.70')

  // https://mvnrepository.com/artifact/org.bouncycastle/bcpkix-jdk15on
  implementation('org.bouncycastle:bcpkix-jdk15on:1.70') {
    exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on'
  }
}

I had to exclude the module bcprov-jdk15on from bcpkix-jdk15on because otherwise I get a module duplicated error. The version of bcprov and bcpkix that I'm using are the ones indicated in the pom.xml of the 5.5.13.3 version of itextpdf.
Also, I checked the jar of bcpkix-jdk15on, and the class in question is in there.

The module-info:

module a.b.c {
    requires javafx.controls;
    requires javafx.fxml;

    requires org.controlsfx.controls;
    requires org.kordamp.bootstrapfx.core;
    requires itextpdf;

    opens a.b.c to javafx.fxml;
    exports a.b.c;
    exports a.b.c.controllers;
    exports a.b.c.dtos;
    opens a.b.c.controllers to javafx.fxml;
}

The line that is throwing the exception:

MakeSignature.signDetached(appearance, bouncyCastleDigest, privateKeySignature, chain, null, null, null, 0, MakeSignature.CryptoStandard.CMS);

And the exception:

Exception in thread "JavaFX Application Thread" java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
        at javafx.fxml/javafx.fxml.FXMLLoader$MethodHandler.invoke(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader$ControllerMethodEventHandler.handle(Unknown Source)
        at javafx.base/com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(Unknown Source)
        at javafx.base/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(Unknown Source)
        at javafx.base/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(Unknown Source)
        at javafx.base/com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(Unknown Source)
        at javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(Unknown Source)
        at javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(Unknown Source)
        at javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(Unknown Source)
        at javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(Unknown Source)
        at javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(Unknown Source)
        at javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(Unknown Source)
        at javafx.base/com.sun.javafx.event.EventUtil.fireEventImpl(Unknown Source)
        at javafx.base/com.sun.javafx.event.EventUtil.fireEvent(Unknown Source)
        at javafx.base/javafx.event.Event.fireEvent(Unknown Source)
        at javafx.graphics/javafx.scene.Node.fireEvent(Unknown Source)
        at javafx.controls/javafx.scene.control.Button.fire(Unknown Source)
        at javafx.controls/com.sun.javafx.scene.control.behavior.ButtonBehavior.mouseReleased(Unknown Source)
        at javafx.controls/com.sun.javafx.scene.control.inputmap.InputMap.handle(Unknown Source)
        at javafx.base/com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(Unknown Source)
        at javafx.base/com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(Unknown Source)
        at javafx.base/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(Unknown Source)
        at javafx.base/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(Unknown Source)
        at javafx.base/com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(Unknown Source)
        at javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(Unknown Source)
        at javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(Unknown Source)
        at javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(Unknown Source)
        at javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(Unknown Source)
        at javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(Unknown Source)
        at javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(Unknown Source)
        at javafx.base/com.sun.javafx.event.EventUtil.fireEventImpl(Unknown Source)
        at javafx.base/com.sun.javafx.event.EventUtil.fireEvent(Unknown Source)
        at javafx.base/javafx.event.Event.fireEvent(Unknown Source)
        at javafx.graphics/javafx.scene.Scene$MouseHandler.process(Unknown Source)
        at javafx.graphics/javafx.scene.Scene.processMouseEvent(Unknown Source)
        at javafx.graphics/javafx.scene.Scene$ScenePeerListener.mouseEvent(Unknown Source)
        at javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(Unknown Source)
        at javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(Unknown Source)
        at java.base/java.security.AccessController.doPrivileged(Unknown Source)
        at javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleMouseEvent$2(Unknown Source)
        at javafx.graphics/com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(Unknown Source)
        at javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler.handleMouseEvent(Unknown Source)
        at javafx.graphics/com.sun.glass.ui.View.handleMouseEvent(Unknown Source)
        at javafx.graphics/com.sun.glass.ui.View.notifyMouse(Unknown Source)
        at javafx.graphics/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
        at javafx.graphics/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(Unknown Source)
        at java.base/java.lang.Thread.run(Unknown Source)
Caused by: java.lang.reflect.InvocationTargetException
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
        at java.base/java.lang.reflect.Method.invoke(Unknown Source)
        at com.sun.javafx.reflect.Trampoline.invoke(Unknown Source)
        at jdk.internal.reflect.GeneratedMethodAccessor2.invoke(Unknown Source)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
        at java.base/java.lang.reflect.Method.invoke(Unknown Source)
        at javafx.base/com.sun.javafx.reflect.MethodUtil.invoke(Unknown Source)
        at javafx.fxml/com.sun.javafx.fxml.MethodHelper.invoke(Unknown Source)
        ... 47 more
Caused by: java.lang.NoClassDefFoundError: org/bouncycastle/cert/X509CertificateHolder
        at a.b.c.merged.module@0.1/com.itextpdf.text.pdf.security.MakeSignature.signDetached(Unknown Source)
        at a.b.c.merged.module@0.1/com.itextpdf.text.pdf.security.MakeSignature.signDetached(Unknown Source)
        at a.b.c@0.1/a.b.c.services.Service.sign(Unknown Source)
        at a.b.c@0.1/a.b.c.services.Service.signSingle(Unknown Source)
        at a.b.c@0.1/a.b.c.controllers.Controller.onSignButtonClick(Unknown Source)
        ... 57 more
Caused by: java.lang.ClassNotFoundException: org.bouncycastle.cert.X509CertificateHolder
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(Unknown Source)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(Unknown Source)
        at java.base/java.lang.ClassLoader.loadClass(Unknown Source)
        ... 62 more

Like I said, the signing works without any issues when I'm running the application on the IDE, but it fails with the compiled image. By the way, to compile the image, I'm using the following command: ./gradlew clean jlinkZip, and then I run the program with the app.bat in the bin folder.

Update 1: I forgot to mention, regarding the duplicated module error that I get if I don't exclude bcprov from bcpkix, I only get this error when compiling with gradle. If I run the application through IDE, I don't get this error.

Update 2: I was comparing the sizes of the compiled images with different dependency configurations, and I noticed that when I add only the bcpkix dependency, the size of the image doesn't increase (more specifically, the "module" file located in the lib folder). This is the dependency that has the class that is missing. Is it possible that gradle isn't compiling the app with this dependency? If so, why? This doesn't happen with the dependency bcprov-jdk15to18, only with bcpkix-jdk15on.

I'd appreciate any help. Thank you.


Solution

  • Bouncy castle software is modular and you have a module. You need to require the software in your module to use it.

    Add this line to your module-info.java:

    requires org.bouncycastle.pkix;
    

    This will require the bouncy castle pkix module. Without requiring the module, the linker won't think the module is used and won't link it into your jlink image.

    You are also using the wrong version of bouncy castle pkix. You should be using:

    org.bouncycastle:bcpkix-jdk18on:1.78.1
    

    Substitute 1.78.1 with whatever the latest current version is.

    When you do that you don't need to exclude an old version of the bouncy castle provider as you are currently doing. You also don't need to explicitly have a dependency on the bouncy castle provider (bcprov-jdk15to18), as the bcpkix dependency will transitively pick up the right dependency automatically (which will not be the jdk15to18 version, which is non-modular and cannot be linked by jlink).

    Even if you do that, I don't know if what you are trying to do will work. The itext version you use is not modular (has no module-info.java) so is being used as an automatic module. It also has optional dependencies on the old jdk8 versions of bouncy castle, which might not be binary compatible with the new bouncy castle versions. Automatic modules are problematic for jlink, usually jlink won't work with automatic modules. I think the gradle jlink plugin tries to workaround this, but I don't use it and don't know how successful it might or might not be at that.

    Luckily, the suggested changes did work, as noted in comments by the asker:

    ... I managed to resolve the issue ... Here is what I did:

    First, I noticed that I wasn't using the latest version of itextpdf: instead of 5.5.13.3, I should've been using the 5.5.13.4 version.

    So I changed it, and then I checked which version of bcpkix that version of itextpdf was using, which was bcpkix-jdk15to18:1.78.1.

    I replaced the previous versions of bcprov and bcpkix I was using with just bcpkix 1.78.1, and it worked.

    Like you said, there's no need to add bcprov or exclude anything that way...