diff --git a/src/main/java/org/jetbrains/nativecerts/NativeTrustedCertificates.java b/src/main/java/org/jetbrains/nativecerts/NativeTrustedCertificates.java index eba0b66..dc1a790 100644 --- a/src/main/java/org/jetbrains/nativecerts/NativeTrustedCertificates.java +++ b/src/main/java/org/jetbrains/nativecerts/NativeTrustedCertificates.java @@ -1,8 +1,7 @@ package org.jetbrains.nativecerts; import org.jetbrains.nativecerts.linux.LinuxTrustedCertificatesUtil; -import org.jetbrains.nativecerts.mac.SecurityFramework; -import org.jetbrains.nativecerts.mac.SecurityFrameworkUtil; +import org.jetbrains.nativecerts.mac.KeyChainStore; import org.jetbrains.nativecerts.win32.Crypt32ExtUtil; import java.security.cert.X509Certificate; @@ -17,7 +16,7 @@ public class NativeTrustedCertificates { /** * Get custom trusted certificates from the operating system. * Uses platform-specific APIs. Does not fail, only logs to java util logging. - * On some systems (currently, Linux) may return an entire set of trusted certificates. + * On some systems (currently, Linux and macOS (on Java >=23) may return an entire set of trusted certificates. *

* To get more logging on user's machine enable FINE logging level for {@code org.jetbrains.nativecerts} category. *

@@ -30,12 +29,7 @@ public static Collection getCustomOsSpecificTrustedCertificates } if (isMac) { - List admin = SecurityFrameworkUtil.getTrustedRoots(SecurityFramework.SecTrustSettingsDomain.admin); - List user = SecurityFrameworkUtil.getTrustedRoots(SecurityFramework.SecTrustSettingsDomain.user); - - Set result = new HashSet<>(admin); - result.addAll(user); - return result; + return KeyChainStore.getAllTrustedCertificates(); } if (isWindows) { diff --git a/src/main/java/org/jetbrains/nativecerts/mac/KeyChainStore.java b/src/main/java/org/jetbrains/nativecerts/mac/KeyChainStore.java new file mode 100644 index 0000000..eaef768 --- /dev/null +++ b/src/main/java/org/jetbrains/nativecerts/mac/KeyChainStore.java @@ -0,0 +1,84 @@ +package org.jetbrains.nativecerts.mac; + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +public class KeyChainStore { + + public static List getPredefinedRootCertificates() { + return getAppleKeyChainStore("KeychainStore-ROOT"); + } + + public static List getCustomTrustedCertificates() { + return getAppleKeyChainStore("KeychainStore"); + } + + public static Collection getAllTrustedCertificates() { + List customTrustedCertificates = getCustomTrustedCertificates(); + List osPredefinedCertificates = getPredefinedRootCertificates(); + + Set result = new HashSet<>(customTrustedCertificates); + result.addAll(osPredefinedCertificates); + + return result; + } + + private static @NotNull List getAppleKeyChainStore(String keyChainStore) { + try { + KeyStore keyStore = KeyStore.getInstance(keyChainStore, "Apple"); + keyStore.load(null, null); + + Iterator iterator = keyStore.aliases().asIterator(); + return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED), false) + .sorted() + .map(alias -> { + try { + return (X509Certificate) keyStore.getCertificate(alias); + } catch (KeyStoreException e) { + throw new RuntimeException(e); + } + }) + .collect(Collectors.toList()); + } catch (KeyStoreException e) { + // only available from Java 23 + if (e.getMessage().equals("KeychainStore-ROOT not found")) { + return SecurityFrameworkUtil.getTrustedRoots(SecurityFramework.SecTrustSettingsDomain.admin); + } + throw new RuntimeException(e); + } catch (NoSuchProviderException | IOException | NoSuchAlgorithmException | + CertificateException e) { + throw new RuntimeException(e); + } + } + + static boolean isSelfSignedCertificate(X509Certificate certificate) { + if (!certificate.getSubjectX500Principal().equals(certificate.getIssuerX500Principal())) { + return false; + } + + try { + certificate.verify(certificate.getPublicKey()); + } catch (Exception e) { + return false; + } + + return true; + } +} diff --git a/src/main/java/org/jetbrains/nativecerts/mac/SecurityFrameworkUtil.java b/src/main/java/org/jetbrains/nativecerts/mac/SecurityFrameworkUtil.java index 216ebcf..fed1b8c 100644 --- a/src/main/java/org/jetbrains/nativecerts/mac/SecurityFrameworkUtil.java +++ b/src/main/java/org/jetbrains/nativecerts/mac/SecurityFrameworkUtil.java @@ -19,9 +19,9 @@ * Get trusted certificates stored in corresponding keychains via Security frameworks APIs. * for the other implementations see root_cgo_darwin.go in Go and trust_store_mac.cc in Chromium *

- * In the future it would be better to implement {@code X509TrustManager} on SecTrustEvaluateWithError instead - * of getting trust chain manually. It's not yet investigated whether it is possible at all to integrate it into - * the SSL framework of JVM. + * This is replaced by {@link KeyChainStore#getAllTrustedCertificates()}, + * it is still internally used on < Java 23 to get predefined + * root certificates. */ public class SecurityFrameworkUtil { private final static Logger LOGGER = Logger.getLogger(SecurityFrameworkUtil.class.getName()); diff --git a/src/test/java/org/jetbrains/nativecerts/mac/SecurityFrameworkUtilTest.java b/src/test/java/org/jetbrains/nativecerts/mac/KeyChainStoreTest.java similarity index 89% rename from src/test/java/org/jetbrains/nativecerts/mac/SecurityFrameworkUtilTest.java rename to src/test/java/org/jetbrains/nativecerts/mac/KeyChainStoreTest.java index b255464..fedb973 100644 --- a/src/test/java/org/jetbrains/nativecerts/mac/SecurityFrameworkUtilTest.java +++ b/src/test/java/org/jetbrains/nativecerts/mac/KeyChainStoreTest.java @@ -12,6 +12,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.security.cert.X509Certificate; +import java.util.Collection; import java.util.Collections; import java.util.List; @@ -30,7 +31,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -public class SecurityFrameworkUtilTest { +public class KeyChainStoreTest { @Rule public final NativeCertsSetupLoggingRule loggingRule = new NativeCertsSetupLoggingRule(); @@ -46,7 +47,7 @@ public void afterTest() { @Test public void enumerateSystemCertificates() { - List trustedRoots = SecurityFrameworkUtil.getTrustedRoots(SecurityFramework.SecTrustSettingsDomain.system); + Collection trustedRoots = KeyChainStore.getAllTrustedCertificates(); System.out.println(trustedRoots.size()); for (X509Certificate root : trustedRoots) { @@ -98,15 +99,15 @@ private void customUserTrustedCertificateTest(@Nullable String policy, String re byte[] encoded = getTestCertificate().getEncoded(); String sha1 = sha1hex(encoded); String sha256 = sha256hex(encoded); - assertEquals("a2133a948547091abc0e0f62aa27bb1927b03f10", sha1); + assertEquals("c64a34966d69b4bed3caa374998a5066ede0f898", sha1); //noinspection SpellCheckingInspection - assertEquals("d5976cf01a27686e61c1ab79907ceed01a9d74a5c7495aad617a7df88fbec204", sha256); + assertEquals("947565b3b4b08c936f0ad5b306062418c61cd2600e109cfdee8318ca69cca16e", sha256); // cleanup just in case it was imported before removeTrustedCert(getTestCertificatePath()); try { - List rootsBefore = SecurityFrameworkUtil.getTrustedRoots(SecurityFramework.SecTrustSettingsDomain.user); + List rootsBefore = KeyChainStore.getCustomTrustedCertificates(); assertFalse(rootsBefore.contains(getTestCertificate())); Assert.assertFalse(verifyCert(getTestCertificatePath(), policy)); @@ -126,7 +127,7 @@ private void customUserTrustedCertificateTest(@Nullable String policy, String re String trustSettings = executeProcessGetStdout(ExitCodeHandling.ASSERT, "/usr/bin/security", "dump-trust-setting"); Assert.assertTrue(trustSettings, trustSettings.contains("certificates-tests.labs.intellij.net")); - List rootsAfter = SecurityFrameworkUtil.getTrustedRoots(SecurityFramework.SecTrustSettingsDomain.user); + List rootsAfter = KeyChainStore.getCustomTrustedCertificates(); assertEquals(shouldTrust, rootsAfter.contains(getTestCertificate())); assertTrue(removeTrustedCert(getTestCertificatePath())); @@ -134,7 +135,7 @@ private void customUserTrustedCertificateTest(@Nullable String policy, String re Thread.sleep(3000); Assert.assertFalse(verifyCert(getTestCertificatePath(), policy)); - List rootsAfterRemoval = SecurityFrameworkUtil.getTrustedRoots(SecurityFramework.SecTrustSettingsDomain.user); + List rootsAfterRemoval = KeyChainStore.getCustomTrustedCertificates(); assertFalse(rootsAfterRemoval.contains(getTestCertificate())); } finally { // even if test fails we must remove trusted certificate @@ -144,7 +145,7 @@ private void customUserTrustedCertificateTest(@Nullable String policy, String re @Test public void testCertificateIsSelfSigned() { - assertTrue(SecurityFrameworkUtil.isSelfSignedCertificate(getTestCertificate())); + assertTrue(KeyChainStore.isSelfSignedCertificate(getTestCertificate())); } private static boolean verifyCert(Path cert, @Nullable String policy) { diff --git a/src/test/java/org/jetbrains/nativecerts/win32/Crypt32ExtUtilTest.java b/src/test/java/org/jetbrains/nativecerts/win32/Crypt32ExtUtilTest.java index b91e7dd..85b6b25 100644 --- a/src/test/java/org/jetbrains/nativecerts/win32/Crypt32ExtUtilTest.java +++ b/src/test/java/org/jetbrains/nativecerts/win32/Crypt32ExtUtilTest.java @@ -60,9 +60,9 @@ public void realUserTrustedCertificateTest() throws Exception { byte[] encoded = getTestCertificate().getEncoded(); String sha1 = sha1hex(encoded); String sha256 = sha256hex(encoded); - assertEquals("a2133a948547091abc0e0f62aa27bb1927b03f10", sha1); + assertEquals("c64a34966d69b4bed3caa374998a5066ede0f898", sha1); //noinspection SpellCheckingInspection - assertEquals("d5976cf01a27686e61c1ab79907ceed01a9d74a5c7495aad617a7df88fbec204", sha256); + assertEquals("947565b3b4b08c936f0ad5b306062418c61cd2600e109cfdee8318ca69cca16e", sha256); // cleanup just in case it was imported before removeTrustedCert(sha1); diff --git a/src/test/resources/certificates-tests.labs.intellij.net.cer b/src/test/resources/certificates-tests.labs.intellij.net.cer index dbf2ff6..5caab96 100644 Binary files a/src/test/resources/certificates-tests.labs.intellij.net.cer and b/src/test/resources/certificates-tests.labs.intellij.net.cer differ