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