diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/SecurityProvidersSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/SecurityProvidersSupport.java index 4700df4c2fa9..e9659b49e5cc 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/SecurityProvidersSupport.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/SecurityProvidersSupport.java @@ -73,12 +73,15 @@ public final class SecurityProvidersSupport { private final EconomicSet userRequestedSecurityProviders = EconomicSet.create(); /** - * A map of providers, identified by their names (see {@link Provider#getName()}), and the - * results of their verification (see javax.crypto.JceSecurity#getVerificationResult). This - * structure is used instead of the (see javax.crypto.JceSecurity#verifyingProviders) map to - * avoid keeping provider objects in the image heap. + * A map of providers, identified by their implementation classes, and the results of their verification + * (see javax.crypto.JceSecurity#getVerificationResult). This structure is used instead of the + * (see javax.crypto.JceSecurity#verifyingProviders) map to avoid keeping provider objects in + * the image heap. */ - private final EconomicMap verifiedSecurityProviders = ImageHeapMap.create("verifiedSecurityProviders"); + private final EconomicMap, Object> verifiedSecurityProviders = ImageHeapMap.create("verifiedSecurityProviders"); + + /** Set of verified provider class names, used when providers are requested by name. */ + private final EconomicSet verifiedSecurityProviderClassNames = EconomicSet.create(); private Properties savedInitialSecurityProperties; @@ -95,12 +98,13 @@ public static SecurityProvidersSupport singleton() { } @Platforms(Platform.HOSTED_ONLY.class) - public void addVerifiedSecurityProvider(String key, Object verificationResult) { - verifiedSecurityProviders.put(key, verificationResult); + public void addVerifiedSecurityProvider(Class providerClass, Object verificationResult) { + verifiedSecurityProviders.put(providerClass, verificationResult); + verifiedSecurityProviderClassNames.add(providerClass.getName()); } - public Object getSecurityProviderVerificationResult(String key) { - return verifiedSecurityProviders.get(key); + public Object getSecurityProviderVerificationResult(Class providerClass) { + return verifiedSecurityProviders.get(providerClass); } @Platforms(Platform.HOSTED_ONLY.class) @@ -119,12 +123,11 @@ public boolean isUserRequestedSecurityProvider(String provider) { } /** - * Returns {@code true} if the provider, identified by either its name (e.g., SUN) or fully - * qualified name (e.g., sun.security.provider.Sun), is either user-requested or reachable via a - * security service. + * Returns {@code true} if the provider, identified by its fully qualified name (e.g., + * sun.security.provider.Sun), is either user-requested or reachable via a security service. */ - public boolean isSecurityProviderRequested(String providerName, String providerFQName) { - return verifiedSecurityProviders.containsKey(providerName) || userRequestedSecurityProviders.contains(providerFQName); + public boolean isSecurityProviderRequested(String providerFQName) { + return verifiedSecurityProviderClassNames.contains(providerFQName) || userRequestedSecurityProviders.contains(providerFQName); } @Platforms(Platform.HOSTED_ONLY.class) @@ -176,7 +179,7 @@ public static String getBuiltInProviderClassName(String provName) { public boolean isMissingBuiltInProvider(String provName) { String providerName = getBuiltInProviderName(provName); String providerFQName = getBuiltInProviderClassName(provName); - return providerName != null && !isSecurityProviderRequested(providerName, providerFQName); + return providerName != null && !isSecurityProviderRequested(providerFQName); } public static SecurityException missingBuiltInProvider(String provName) { @@ -194,15 +197,15 @@ public static SecurityException missingBuiltInProvider(String provName) { public Provider loadBuiltInProvider(String provName, Debug debug) { return switch (provName) { case "SUN", "sun.security.provider.Sun" -> - isSecurityProviderRequested("SUN", "sun.security.provider.Sun") ? new sun.security.provider.Sun() : null; + isSecurityProviderRequested("sun.security.provider.Sun") ? new sun.security.provider.Sun() : null; case "SunRsaSign", "sun.security.rsa.SunRsaSign" -> - isSecurityProviderRequested("SunRsaSign", "sun.security.rsa.SunRsaSign") ? new sun.security.rsa.SunRsaSign() : null; + isSecurityProviderRequested("sun.security.rsa.SunRsaSign") ? new sun.security.rsa.SunRsaSign() : null; case "SunJCE", "com.sun.crypto.provider.SunJCE" -> - isSecurityProviderRequested("SunJCE", "com.sun.crypto.provider.SunJCE") ? new com.sun.crypto.provider.SunJCE() : null; + isSecurityProviderRequested("com.sun.crypto.provider.SunJCE") ? new com.sun.crypto.provider.SunJCE() : null; case "SunJSSE", "sun.security.ssl.SunJSSE" -> - isSecurityProviderRequested("SunJSSE", "sun.security.ssl.SunJSSE") ? new sun.security.ssl.SunJSSE() : null; + isSecurityProviderRequested("sun.security.ssl.SunJSSE") ? new sun.security.ssl.SunJSSE() : null; case "SunEC", "sun.security.ec.SunEC" -> - isSecurityProviderRequested("SunEC", "sun.security.ec.SunEC") ? allocateSunECProvider() : null; + isSecurityProviderRequested("sun.security.ec.SunEC") ? allocateSunECProvider() : null; case "Apple", "apple.security.AppleProvider" -> { try { Class c = Class.forName("apple.security.AppleProvider"); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/runtimeinit/SecuritySubstitutionRuntimeInit.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/runtimeinit/SecuritySubstitutionRuntimeInit.java index 53c258e7c11f..74032ffb9fa7 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/runtimeinit/SecuritySubstitutionRuntimeInit.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/runtimeinit/SecuritySubstitutionRuntimeInit.java @@ -26,20 +26,29 @@ package com.oracle.svm.core.jdk.runtimeinit; import java.net.URL; +import java.security.NoSuchProviderException; import java.security.Provider; +import java.util.IdentityHashMap; import java.util.Map; import java.util.Properties; import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; import com.oracle.svm.core.annotate.Alias; import com.oracle.svm.core.annotate.RecomputeFieldValue; import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetElement; import com.oracle.svm.core.annotate.TargetClass; +import com.oracle.svm.core.hub.DynamicHub; +import com.oracle.svm.core.hub.RuntimeClassLoading; import com.oracle.svm.core.jdk.SecurityProvidersInitializedAtRunTime; import com.oracle.svm.core.jdk.SecurityProvidersSupport; +import com.oracle.svm.core.jdk.UnsupportedFeatureError; import com.oracle.svm.shared.util.BasedOnJDKFile; import jdk.graal.compiler.core.common.SuppressFBWarnings; +import sun.security.util.Debug; @TargetClass(value = java.security.Security.class, onlyWith = SecurityProvidersInitializedAtRunTime.class) final class Target_java_security_Security { @@ -74,32 +83,60 @@ private static void loadMaster() { @SuppressWarnings({"unused"}) final class Target_javax_crypto_JceSecurity { + // Checkstyle: stop + @Alias // + static Object PROVIDER_VERIFIED; + // Checkstyle: resume + + @Alias // + static Debug debug; + /* * Map of providers that have already been verified. A value of PROVIDER_VERIFIED * indicates successful verification. Otherwise, the value is the Exception that caused the * verification to fail. */ @Alias // - @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.Reset) // - private static Map verificationResults; + @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.NewInstance, declClass = ConcurrentHashMap.class) // + static Map verificationResults; @Alias // - @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.Reset) // - private static Map verifyingProviders; + @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.NewInstance, declClass = IdentityHashMap.class) // + static Map verifyingProviders; @Alias // @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.FromAlias) // private static Map, URL> codeBaseCacheRef = new WeakHashMap<>(); + @Alias // + @TargetElement // + static java.lang.ref.ReferenceQueue queue; + + @Alias // + static native void expungeStaleWrappers(); + + @Alias // + static native URL getCodeBase(Class clazz); + + @Alias // + static native void verifyProvider(URL codeBase, Provider p) throws Exception; + @Substitute static Exception getVerificationResult(Provider p) { - /* The verification results map key is an identity wrapper object. */ - Object o = SecurityProvidersSupport.singleton().getSecurityProviderVerificationResult(p.getName()); + /* Provider verification is tied to the provider implementation class. */ + Object o = SecurityProvidersSupport.singleton().getSecurityProviderVerificationResult(p.getClass()); if (o == Boolean.TRUE) { return null; } else if (o != null) { return (Exception) o; } + if (RuntimeClassLoading.isSupported() && DynamicHub.fromClass(p.getClass()).isRuntimeLoaded()) { + /* + * Providers loaded by Crema at run time cannot have a build-time verification result, + * so they use the JDK verifier path and its provider-identity cache. + */ + return JceSecurityRuntimeLoadedProviderVerifier.getVerificationResult(p); + } /* * If the verification result is not found in the verificationResults map, HotSpot will * attempt to verify the provider. This requires accessing the code base, which isn't @@ -114,6 +151,62 @@ static Exception getVerificationResult(Provider p) { } } +final class JceSecurityRuntimeLoadedProviderVerifier { + + private JceSecurityRuntimeLoadedProviderVerifier() { + } + + /** Verifies runtime-loaded providers using the JDK verifier algorithm. */ + static Exception getVerificationResult(Provider p) { + Target_javax_crypto_JceSecurity.expungeStaleWrappers(); + Object pKey = new Target_javax_crypto_JceSecurity_WeakIdentityWrapper(p, Target_javax_crypto_JceSecurity.queue); + try { + Object o = Target_javax_crypto_JceSecurity.verificationResults.computeIfAbsent(pKey, new Function<>() { + @Override + public Object apply(Object key) { + if (Target_javax_crypto_JceSecurity.verifyingProviders.get(p) != null) { + throw new IllegalStateException(); + } + Object result; + try { + Target_javax_crypto_JceSecurity.verifyingProviders.put(p, Boolean.FALSE); + URL providerURL = Target_javax_crypto_JceSecurity.getCodeBase(p.getClass()); + Target_javax_crypto_JceSecurity.verifyProvider(providerURL, p); + result = Target_javax_crypto_JceSecurity.PROVIDER_VERIFIED; + } catch (UnsupportedFeatureError e) { + /* + * OracleJDK can route provider verification through JarVerifier, which is + * intentionally unsupported in Native Image. OpenJDK's provider verifier is + * open for this case, so runtime-loaded providers fall back to that behavior. + */ + result = Target_javax_crypto_JceSecurity.PROVIDER_VERIFIED; + } catch (Exception e) { + result = e; + } finally { + Target_javax_crypto_JceSecurity.verifyingProviders.remove(p); + } + if (Target_javax_crypto_JceSecurity.debug != null) { + Target_javax_crypto_JceSecurity.debug.println("Provider " + p.getName() + " verification result: " + result); + } + return result; + } + }); + return o == Target_javax_crypto_JceSecurity.PROVIDER_VERIFIED ? null : (Exception) o; + } catch (IllegalStateException ise) { + return new NoSuchProviderException("Recursion during verification"); + } + } +} + +@TargetClass(className = "javax.crypto.JceSecurity", innerClass = "WeakIdentityWrapper", onlyWith = SecurityProvidersInitializedAtRunTime.class) +@SuppressWarnings({"unused"}) +final class Target_javax_crypto_JceSecurity_WeakIdentityWrapper { + + @Alias // + Target_javax_crypto_JceSecurity_WeakIdentityWrapper(Provider obj, java.lang.ref.ReferenceQueue queue) { + } +} + @TargetClass(className = "sun.security.jca.ProviderConfig", onlyWith = SecurityProvidersInitializedAtRunTime.class) @SuppressWarnings({"unused", "static-method"}) final class Target_sun_security_jca_ProviderConfig { diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/SecurityServicesFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/SecurityServicesFeature.java index 05b6c5371cf6..b48ec3302254 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/SecurityServicesFeature.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/SecurityServicesFeature.java @@ -844,16 +844,15 @@ private void registerProvider(DuringAnalysisAccess access, Provider provider) { * Exception, in case of failure. Null is interpreted as Boolean.TRUE at * runtime, signifying successful verification. */ - String providerName = provider.getName(); + String providerFQName = provider.getClass().getName(); SecurityProvidersSupport support = SecurityProvidersSupport.singleton(); - support.addVerifiedSecurityProvider(providerName, result instanceof Exception ? result : Boolean.TRUE); + support.addVerifiedSecurityProvider(provider.getClass(), result instanceof Exception ? result : Boolean.TRUE); /* * If this provider is not yet loaded via the service loading mechanism, we need * to manually prepare reflection metadata now, so that service loading works at * runtime (see sun.security.jca.ProviderConfig.doLoadProvider). */ - String providerFQName = provider.getClass().getName(); if (support.isSecurityProviderNotLoaded(providerFQName)) { Set registeredProviders = new HashSet<>(); // noEconomicSet(unimplemented) ServiceLoaderFeature.registerProviderForRuntimeReflectionAccess(access, providerFQName, registeredProviders); diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/services/SecurityProviderVerificationTest.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/services/SecurityProviderVerificationTest.java new file mode 100644 index 000000000000..b5cc048a3a06 --- /dev/null +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/services/SecurityProviderVerificationTest.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.test.services; + +import java.security.AlgorithmParameters; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.Security; +import java.security.spec.AlgorithmParameterSpec; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.CipherSpi; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.ShortBufferException; + +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Test; + +import com.oracle.svm.core.FutureDefaultsOptions; +import com.oracle.svm.test.NativeImageBuildArgs; + +/** + * Tests that JCE provider verification results are bound to provider implementation classes. + */ +@NativeImageBuildArgs({ + "--future-defaults=run-time-initialize-security-providers", + "-H:+UnlockExperimentalVMOptions", + "-H:AdditionalSecurityProviders=com.oracle.svm.test.services.SecurityProviderVerificationTest$BuildTimeProvider", + "-H:-UnlockExperimentalVMOptions" +}) +public class SecurityProviderVerificationTest { + private static final String PROVIDER_NAME = "same-name-test-provider"; + private static final String CIPHER_ALGORITHM = "SameNameCipher"; + + @Test + public void testSameNameProviderDoesNotReuseVerificationResult() throws Exception { + Assume.assumeTrue("needs runtime security-provider initialization", FutureDefaultsOptions.securityProvidersInitializedAtRunTime()); + + Security.removeProvider(PROVIDER_NAME); + Provider runtimeProvider = new RuntimeProvider(); + try { + Security.addProvider(runtimeProvider); + Cipher.getInstance(CIPHER_ALGORITHM, runtimeProvider); + Assert.fail("A provider must not reuse another provider class' verification result only because the provider names match."); + } catch (SecurityException e) { + Assert.assertTrue("Error should identify the unverified provider class.", + e.getMessage().contains(RuntimeProvider.class.getName())); + } finally { + Security.removeProvider(PROVIDER_NAME); + } + } + + /** + * Provider that is verified during image generation. + */ + public static final class BuildTimeProvider extends Provider { + static final long serialVersionUID = 1L; + + @SuppressWarnings("deprecation") + public BuildTimeProvider() { + super(PROVIDER_NAME, 1.0, "Provider verified during image generation."); + putService(new Service(this, "Cipher", CIPHER_ALGORITHM, NoOpCipher.class.getName(), null, null)); + } + } + + /** + * Provider with the same provider name but a different implementation class. + */ + public static final class RuntimeProvider extends Provider { + static final long serialVersionUID = 1L; + + @SuppressWarnings("deprecation") + public RuntimeProvider() { + super(PROVIDER_NAME, 1.0, "Provider registered only at run time."); + putService(new Service(this, "Cipher", CIPHER_ALGORITHM, NoOpCipher.class.getName(), null, null)); + } + } + + /** + * Minimal cipher implementation used only to reach JCE provider verification. + */ + public static final class NoOpCipher extends CipherSpi { + @Override + protected void engineSetMode(String mode) throws NoSuchAlgorithmException { + } + + @Override + protected void engineSetPadding(String padding) throws NoSuchPaddingException { + } + + @Override + protected int engineGetBlockSize() { + return 1; + } + + @Override + protected int engineGetOutputSize(int inputLen) { + return inputLen; + } + + @Override + protected byte[] engineGetIV() { + return new byte[0]; + } + + @Override + protected AlgorithmParameters engineGetParameters() { + return null; + } + + @Override + protected void engineInit(int opmode, Key key, java.security.SecureRandom random) throws InvalidKeyException { + } + + @Override + protected void engineInit(int opmode, Key key, AlgorithmParameterSpec params, java.security.SecureRandom random) throws InvalidKeyException, InvalidAlgorithmParameterException { + } + + @Override + protected void engineInit(int opmode, Key key, AlgorithmParameters params, java.security.SecureRandom random) throws InvalidKeyException, InvalidAlgorithmParameterException { + } + + @Override + protected byte[] engineUpdate(byte[] input, int inputOffset, int inputLen) { + return input == null ? new byte[0] : input.clone(); + } + + @Override + protected int engineUpdate(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) throws ShortBufferException { + return 0; + } + + @Override + protected byte[] engineDoFinal(byte[] input, int inputOffset, int inputLen) throws IllegalBlockSizeException, BadPaddingException { + return input == null ? new byte[0] : input.clone(); + } + + @Override + protected int engineDoFinal(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) throws ShortBufferException, IllegalBlockSizeException, BadPaddingException { + return 0; + } + } +}