diff --git a/.github/workflows/filtered-providers.yml b/.github/workflows/filtered-providers.yml
index c70fde66b..9c8b95860 100644
--- a/.github/workflows/filtered-providers.yml
+++ b/.github/workflows/filtered-providers.yml
@@ -70,7 +70,8 @@ jobs:
"com/wolfssl/security/providers/FilteredSunEC.class" \
"com/wolfssl/security/providers/FilteredSunRsaSign.class" \
"com/wolfssl/security/providers/ProviderServiceCopier.class" \
- "META-INF/services/java.security.Provider"; do
+ "META-INF/services/java.security.Provider" \
+ "module-info.class"; do
if jar tf "$JAR" | grep -qx "$entry"; then
echo "OK : present -> $entry"
else
@@ -80,6 +81,16 @@ jobs:
if jar tf "$JAR" | grep -qE '^com/wolfssl/wolfcrypt/.*\.class$'; then
echo "ERR: wolfCrypt classes leaked into ant jar"; fail=1
fi
+ # Explicit module com.wolfssl.security.providers
+ DESC=$(jar --describe-module --file="$JAR")
+ echo "$DESC"
+ if echo "$DESC" | grep -qE '^com\.wolfssl\.security\.providers jar:' && \
+ ! echo "$DESC" | grep -q 'automatic'; then
+ echo "OK : explicit module com.wolfssl.security.providers"
+ else
+ echo "ERR: ant jar is not module com.wolfssl.security.providers"
+ fail=1
+ fi
[ "$fail" -eq 0 ] || { echo "FAILURE: ant jar verification"; exit 1; }
echo "SUCCESS: ant jar verification passed"
@@ -98,7 +109,8 @@ jobs:
"com/wolfssl/security/providers/FilteredSunEC.class" \
"com/wolfssl/security/providers/FilteredSunRsaSign.class" \
"com/wolfssl/security/providers/ProviderServiceCopier.class" \
- "META-INF/services/java.security.Provider"; do
+ "META-INF/services/java.security.Provider" \
+ "module-info.class"; do
if jar tf "$JAR" | grep -qx "$entry"; then
echo "OK : present -> $entry"
else
@@ -108,6 +120,17 @@ jobs:
if jar tf "$JAR" | grep -qE '^com/wolfssl/wolfcrypt/.*\.class$'; then
echo "ERR: wolfCrypt classes leaked into Maven classified jar"; fail=1
fi
+ # Explicit module com.wolfssl.security.providers (Maven adds @version)
+ DESC=$(jar --describe-module --file="$JAR")
+ echo "$DESC"
+ if echo "$DESC" | \
+ grep -qE '^com\.wolfssl\.security\.providers(@[^ ]*)? jar:' && \
+ ! echo "$DESC" | grep -q 'automatic'; then
+ echo "OK : explicit module com.wolfssl.security.providers"
+ else
+ echo "ERR: Maven jar is not module com.wolfssl.security.providers"
+ fail=1
+ fi
[ "$fail" -eq 0 ] || { echo "FAILURE: Maven jar verification"; exit 1; }
echo "SUCCESS: Maven jar verification passed"
diff --git a/build.xml b/build.xml
index 4ced1be5d..c0b46f045 100644
--- a/build.xml
+++ b/build.xml
@@ -37,6 +37,8 @@
+
+
+ (plus their ServiceLoader registration and module-info.class) into
+ filtered-providers.jar. Java 9+ only, no-op on Java 8. -->
+
+
+
@@ -487,6 +494,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/filtered-providers/README.md b/examples/filtered-providers/README.md
index fcfa1496c..56e1b1aa0 100644
--- a/examples/filtered-providers/README.md
+++ b/examples/filtered-providers/README.md
@@ -49,6 +49,11 @@ Both jars are self-contained and include a
`META-INF/services/java.security.Provider` entry so the providers can also be
discovered via `ServiceLoader`.
+Both jars are also the JPMS module `com.wolfssl.security.providers`, with a
+module descriptor compiled from
+[java9/module-info.java](java9/module-info.java). On the class path the module
+descriptor is ignored.
+
## Pulling into your own Linux host, VM, or container
The build produces a single self-contained jar. Copy it out and drop it onto
@@ -102,6 +107,29 @@ your system:
For Docker/Kubernetes, set these via `JAVA_TOOL_OPTIONS`. See
[docs/add-opens.md](docs/add-opens.md) for the per-flag rationale.
+ **Module path alternative:** the jar can be placed on the module path
+ instead of the class path. By doing this, applications don't need to add it
+ to their own class path, and JDK tools like `keytool`, which ignore the
+ `CLASSPATH` environment variable, still load the providers. In the module
+ path case, resolve the module explicitly and target the flags at the module
+ name instead of `ALL-UNNAMED`. For JDK 22+:
+
+ ```
+ --module-path=/usr/share/java/filtered-providers.jar
+ --add-modules=com.wolfssl.security.providers
+ --add-exports=java.base/sun.security.ec=com.wolfssl.security.providers
+ --add-opens=java.base/sun.security.ec=com.wolfssl.security.providers
+ --add-opens=java.base/java.security=com.wolfssl.security.providers
+ --add-opens=java.base/sun.security.provider=com.wolfssl.security.providers
+ --add-opens=java.base/sun.security.util=com.wolfssl.security.providers
+ --add-opens=java.base/sun.security.rsa=com.wolfssl.security.providers
+ ```
+
+ For JDK 9-21, make the same substitution in the JDK 9-21 EC flags
+ (`jdk.crypto.ec/sun.security.ec=com.wolfssl.security.providers`). Flags
+ that still target `ALL-UNNAMED` do not reach a named module, and the
+ providers fail to initialize with `IllegalAccessException`.
+
4. Optional debug logging:
```
diff --git a/examples/filtered-providers/docs/add-opens.md b/examples/filtered-providers/docs/add-opens.md
index f1dfc73d2..e90350f4e 100644
--- a/examples/filtered-providers/docs/add-opens.md
+++ b/examples/filtered-providers/docs/add-opens.md
@@ -61,3 +61,21 @@ The `ant test-filtered-providers` target and the Maven `filtered-providers`
profile select the correct EC flag set automatically based on the running
JDK.
+## Flag target: class path vs module path
+
+The flags above use `ALL-UNNAMED`, which covers the jar when it is on the
+class path, where the providers run in the unnamed module.
+
+When `filtered-providers.jar` is on the module path, the providers run in the
+named module `com.wolfssl.security.providers`. `ALL-UNNAMED` does not include
+named modules, so every flag above must target the module name instead, for
+example:
+
+```
+--add-opens=java.base/java.security=com.wolfssl.security.providers
+```
+
+The module must also be resolved with
+`--add-modules=com.wolfssl.security.providers`. `ant test-filtered-providers`
+runs the tests in both modes.
+
diff --git a/examples/filtered-providers/java9/module-info.java b/examples/filtered-providers/java9/module-info.java
new file mode 100644
index 000000000..45e4b6400
--- /dev/null
+++ b/examples/filtered-providers/java9/module-info.java
@@ -0,0 +1,42 @@
+/* module-info.java
+ *
+ * Copyright (C) 2006-2026 wolfSSL Inc.
+ *
+ * This file is part of wolfSSL.
+ *
+ * wolfSSL is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * wolfSSL is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
+ */
+
+/**
+ * Filtered Sun security providers module (FilteredSun, FilteredSunEC,
+ * FilteredSunRsaSign).
+ *
+ * On the module path, --add-opens / --add-exports flags must target this
+ * module name (see docs/add-opens.md). Ignored on the class path.
+ *
+ * Kept outside src/ because the Maven filtered-providers profile compiles
+ * src/ into target/classes, which holds wolfcrypt-jni's own module-info.
+ */
+module com.wolfssl.security.providers {
+
+ /* Allows explicit use, ex: Security.addProvider(new FilteredSun()) */
+ exports com.wolfssl.security.providers;
+
+ /* Keep in sync with src/META-INF/services/java.security.Provider */
+ provides java.security.Provider with
+ com.wolfssl.security.providers.FilteredSun,
+ com.wolfssl.security.providers.FilteredSunEC,
+ com.wolfssl.security.providers.FilteredSunRsaSign;
+}
diff --git a/pom.xml b/pom.xml
index f9e32d0a3..defe813c9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -16,6 +16,7 @@
without a compiled wolfSSL/wolfCrypt library:
mvn test -Dmain.tests.skip=true -->
false
+ 3.16.0
@@ -49,7 +50,7 @@
org.apache.maven.pluginsmaven-compiler-plugin
- 3.11.0
+ ${maven-compiler-plugin.version}1.81.8
@@ -112,7 +113,7 @@
org.apache.maven.pluginsmaven-compiler-plugin
- 3.11.0
+ ${maven-compiler-plugin.version}com/wolfssl/provider/jce/WolfCryptMlKemKem.java
@@ -132,7 +133,7 @@
org.apache.maven.pluginsmaven-compiler-plugin
- 3.11.0
+ ${maven-compiler-plugin.version}
@@ -172,7 +173,7 @@
org.apache.maven.pluginsmaven-compiler-plugin
- 3.11.0
+ ${maven-compiler-plugin.version}compile-java15-eddsa-overlay
@@ -255,7 +256,7 @@
org.apache.maven.pluginsmaven-compiler-plugin
- 3.11.0
+ ${maven-compiler-plugin.version}
@@ -274,6 +275,28 @@
false
+
+
+ compile-filtered-providers-module-info
+ prepare-package
+
+ compile
+
+
+ 9
+
+ ${project.basedir}/examples/filtered-providers/java9
+
+ ${project.build.directory}/filtered-providers-classes
+ false
+
+
@@ -299,11 +322,17 @@
providers in module
com.wolfssl.wolfcrypt, where the
ALL-UNNAMED add-opens below would not
- reach them. The providers ship as a
- plain classpath jar in real
- deployments, which this matches. -->
+ reach them. Module path use is tested
+ by ant test-filtered-providers. -->
false${filtered.ec.flags} --add-opens=java.base/java.security=ALL-UNNAMED --add-opens=java.base/sun.security.provider=ALL-UNNAMED --add-opens=java.base/sun.security.util=ALL-UNNAMED --add-opens=java.base/sun.security.rsa=ALL-UNNAMED
+
+
+ ${project.build.directory}/filtered-providers-classes
+ com/wolfssl/security/providers/test/**
@@ -327,7 +356,7 @@
assemble-filtered-providers-jar-dir
- prepare-package
+ process-classescopy-resources
@@ -355,8 +384,8 @@
+ assembled directory above (provider classes,
+ ServiceLoader registration and module-info). -->
org.apache.maven.pluginsmaven-jar-plugin
diff --git a/src/test/java/com/wolfssl/security/providers/test/FilteredProviderServiceLoaderTest.java b/src/test/java/com/wolfssl/security/providers/test/FilteredProviderServiceLoaderTest.java
new file mode 100644
index 000000000..df20ab89e
--- /dev/null
+++ b/src/test/java/com/wolfssl/security/providers/test/FilteredProviderServiceLoaderTest.java
@@ -0,0 +1,201 @@
+/* FilteredProviderServiceLoaderTest.java
+ *
+ * Copyright (C) 2006-2026 wolfSSL Inc.
+ *
+ * This file is part of wolfSSL.
+ *
+ * wolfSSL is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * wolfSSL is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
+ */
+
+package com.wolfssl.security.providers.test;
+
+import static org.junit.Assert.*;
+import org.junit.Assume;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+
+import java.security.Provider;
+import java.security.Security;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.ServiceLoader;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+
+import com.wolfssl.wolfcrypt.test.TimedTestWatcher;
+
+/**
+ * ServiceLoader discovery test for the filtered Sun security providers.
+ *
+ * Verifies that FilteredSun, FilteredSunEC and FilteredSunRsaSign are found
+ * through ServiceLoader.load(Provider.class), without constructing them
+ * directly. JDK tools such as keytool find providers this way.
+ *
+ * On the class path, discovery uses META-INF/services/java.security.Provider.
+ * On the module path (ant test-filtered-providers, second run), discovery
+ * uses only the module-info "provides" clause, so this test fails if a
+ * provider is missing from it.
+ *
+ * Requires Java 9+.
+ */
+public class FilteredProviderServiceLoaderTest {
+
+ private static final String MODULE_NAME =
+ "com.wolfssl.security.providers";
+
+ /** Expected provider class name to provider name. */
+ private static final Map EXPECTED = new HashMap<>();
+ static {
+ EXPECTED.put("com.wolfssl.security.providers.FilteredSun",
+ "FilteredSun");
+ EXPECTED.put("com.wolfssl.security.providers.FilteredSunEC",
+ "FilteredSunEC");
+ EXPECTED.put("com.wolfssl.security.providers.FilteredSunRsaSign",
+ "FilteredSunRsaSign");
+ }
+
+ /** Properties pinned to benign values while instantiating providers. */
+ private static final String[] PIN_PROPS = {
+ "wolfssl.filtered.useOriginalNames",
+ "wolfssl.filtered.sun.additionalServices",
+ "wolfssl.filtered.sunec.additionalServices",
+ "wolfssl.filtered.sunrsasign.additionalServices" };
+
+ /** Benign pin values for PIN_PROPS. */
+ private static final String[] PIN_VALS = { "false", "", "", "" };
+
+ @Rule(order = Integer.MIN_VALUE)
+ public TestRule testWatcher = TimedTestWatcher.create();
+
+ @BeforeClass
+ public static void checkJavaVersion() {
+ Assume.assumeTrue("FilteredSun* providers require Java 9 or greater",
+ javaMajorVersion() >= 9);
+
+ System.out.println("FilteredSun* provider ServiceLoader test");
+ }
+
+ /**
+ * Parse the JDK feature/major version from "java.specification.version"
+ * ("1.8" = 8, "9" = 9, "17" = 17, ...).
+ */
+ private static int javaMajorVersion() {
+
+ String v = System.getProperty("java.specification.version");
+ if (v == null) {
+ return 0;
+ }
+
+ if (v.startsWith("1.")) {
+ v = v.substring(2);
+ }
+
+ int dot = v.indexOf('.');
+ if (dot >= 0) {
+ v = v.substring(0, dot);
+ }
+
+ try {
+ return Integer.parseInt(v);
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ }
+
+ /**
+ * Return the ServiceLoader entries for the filtered providers. Uses
+ * stream() and type() so other providers on the class path (for example
+ * wolfJCE, which needs its native library) are not instantiated.
+ */
+ private static Map>
+ findFilteredProviders() {
+
+ return ServiceLoader.load(Provider.class).stream()
+ .filter(p -> EXPECTED.containsKey(p.type().getName()))
+ .collect(Collectors.toMap(p -> p.type().getName(), p -> p,
+ (a, b) -> a));
+ }
+
+ @Test
+ public void testAllProvidersDiscoverable() {
+
+ Map> found =
+ findFilteredProviders();
+
+ assertEquals("ServiceLoader should return all filtered providers",
+ new TreeSet<>(EXPECTED.keySet()), new TreeSet<>(found.keySet()));
+ }
+
+ @Test
+ public void testDiscoveredProvidersInstantiate() {
+
+ Map> found =
+ findFilteredProviders();
+
+ /* Pin properties so names stay FilteredSun* and no extra services
+ * are granted, even if the test JVM's java.security sets them.
+ * Restore afterward. */
+ String[] prev = new String[PIN_PROPS.length];
+ for (int i = 0; i < PIN_PROPS.length; i++) {
+ prev[i] = Security.getProperty(PIN_PROPS[i]);
+ Security.setProperty(PIN_PROPS[i], PIN_VALS[i]);
+ }
+ try {
+ for (Map.Entry e : EXPECTED.entrySet()) {
+ ServiceLoader.Provider sp = found.get(e.getKey());
+ assertNotNull("Not discovered: " + e.getKey(), sp);
+
+ Provider p = sp.get();
+ assertNotNull("ServiceLoader returned null for " +
+ e.getKey(), p);
+ assertEquals("Provider name for " + e.getKey(),
+ e.getValue(), p.getName());
+ }
+ } finally {
+ for (int i = 0; i < PIN_PROPS.length; i++) {
+ Security.setProperty(PIN_PROPS[i],
+ (prev[i] != null) ? prev[i] : PIN_VALS[i]);
+ }
+ }
+ }
+
+ /**
+ * When the module is in the boot layer (module path run), the providers
+ * must come from it, not from a copy on the class path. Otherwise they
+ * are on the class path, in the unnamed module.
+ */
+ @Test
+ public void testProvidersFromExpectedModule() {
+
+ Optional module = ModuleLayer.boot().findModule(MODULE_NAME);
+
+ for (ServiceLoader.Provider sp :
+ findFilteredProviders().values()) {
+ Module actual = sp.type().getModule();
+ if (module.isPresent()) {
+ assertEquals("Module of " + sp.type().getName(),
+ module.get(), actual);
+ }
+ else {
+ assertFalse("Expected unnamed module for " +
+ sp.type().getName() + ", got " + actual.getName(),
+ actual.isNamed());
+ }
+ }
+ }
+}