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.plugins maven-compiler-plugin - 3.11.0 + ${maven-compiler-plugin.version} 1.8 1.8 @@ -112,7 +113,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.11.0 + ${maven-compiler-plugin.version} com/wolfssl/provider/jce/WolfCryptMlKemKem.java @@ -132,7 +133,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.11.0 + ${maven-compiler-plugin.version} @@ -172,7 +173,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.11.0 + ${maven-compiler-plugin.version} compile-java15-eddsa-overlay @@ -255,7 +256,7 @@ org.apache.maven.plugins maven-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-classes copy-resources @@ -355,8 +384,8 @@ + assembled directory above (provider classes, + ServiceLoader registration and module-info). --> org.apache.maven.plugins maven-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()); + } + } + } +}