Skip to content

Switch macOS native certs to retrieve from JDK (maintain root compat with <23) #7

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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.
* <p>
* To get more logging on user's machine enable FINE logging level for {@code org.jetbrains.nativecerts} category.
* </p>
Expand All @@ -30,12 +29,7 @@ public static Collection<X509Certificate> getCustomOsSpecificTrustedCertificates
}

if (isMac) {
List<X509Certificate> admin = SecurityFrameworkUtil.getTrustedRoots(SecurityFramework.SecTrustSettingsDomain.admin);
List<X509Certificate> user = SecurityFrameworkUtil.getTrustedRoots(SecurityFramework.SecTrustSettingsDomain.user);

Set<X509Certificate> result = new HashSet<>(admin);
result.addAll(user);
return result;
return KeyChainStore.getAllTrustedCertificates();
}

if (isWindows) {
Expand Down
84 changes: 84 additions & 0 deletions src/main/java/org/jetbrains/nativecerts/mac/KeyChainStore.java
Original file line number Diff line number Diff line change
@@ -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<X509Certificate> getPredefinedRootCertificates() {
return getAppleKeyChainStore("KeychainStore-ROOT");
}

public static List<X509Certificate> getCustomTrustedCertificates() {
return getAppleKeyChainStore("KeychainStore");
}

public static Collection<X509Certificate> getAllTrustedCertificates() {
List<X509Certificate> customTrustedCertificates = getCustomTrustedCertificates();
List<X509Certificate> osPredefinedCertificates = getPredefinedRootCertificates();

Set<X509Certificate> result = new HashSet<>(customTrustedCertificates);
result.addAll(osPredefinedCertificates);

return result;
}

private static @NotNull List<X509Certificate> getAppleKeyChainStore(String keyChainStore) {
try {
KeyStore keyStore = KeyStore.getInstance(keyChainStore, "Apple");
keyStore.load(null, null);

Iterator<String> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
* <br><br>
* In the future it would be better to implement {@code X509TrustManager} on <a href="https://developer.apple.com/documentation/security/2980705-sectrustevaluatewitherror">SecTrustEvaluateWithError</a> 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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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();

Expand All @@ -46,7 +47,7 @@ public void afterTest() {

@Test
public void enumerateSystemCertificates() {
List<X509Certificate> trustedRoots = SecurityFrameworkUtil.getTrustedRoots(SecurityFramework.SecTrustSettingsDomain.system);
Collection<X509Certificate> trustedRoots = KeyChainStore.getAllTrustedCertificates();

System.out.println(trustedRoots.size());
for (X509Certificate root : trustedRoots) {
Expand Down Expand Up @@ -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<X509Certificate> rootsBefore = SecurityFrameworkUtil.getTrustedRoots(SecurityFramework.SecTrustSettingsDomain.user);
List<X509Certificate> rootsBefore = KeyChainStore.getCustomTrustedCertificates();
assertFalse(rootsBefore.contains(getTestCertificate()));

Assert.assertFalse(verifyCert(getTestCertificatePath(), policy));
Expand All @@ -126,15 +127,15 @@ 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<X509Certificate> rootsAfter = SecurityFrameworkUtil.getTrustedRoots(SecurityFramework.SecTrustSettingsDomain.user);
List<X509Certificate> rootsAfter = KeyChainStore.getCustomTrustedCertificates();
assertEquals(shouldTrust, rootsAfter.contains(getTestCertificate()));

assertTrue(removeTrustedCert(getTestCertificatePath()));
// verify cert is async
Thread.sleep(3000);
Assert.assertFalse(verifyCert(getTestCertificatePath(), policy));

List<X509Certificate> rootsAfterRemoval = SecurityFrameworkUtil.getTrustedRoots(SecurityFramework.SecTrustSettingsDomain.user);
List<X509Certificate> rootsAfterRemoval = KeyChainStore.getCustomTrustedCertificates();
assertFalse(rootsAfterRemoval.contains(getTestCertificate()));
} finally {
// even if test fails we must remove trusted certificate
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Binary file modified src/test/resources/certificates-tests.labs.intellij.net.cer
Binary file not shown.