Skip to content
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

8347067: Load certificates without explicit trust settings in KeyChainStore #22911

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
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
12 changes: 3 additions & 9 deletions src/java.base/macosx/classes/apple/security/KeychainStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -878,15 +878,9 @@ private void createTrustedCertEntry(String alias, List<String> inputTrust,
}

if (tce.trustSettings.isEmpty()) {
if (isSelfSigned) {
// If a self-signed certificate has trust settings without specific entries,
// trust it for all purposes
tce.trustedKeyUsageValue = KnownOIDs.anyExtendedKeyUsage.value();
} else {
// Otherwise, return immediately. The certificate is not
// added into entries.
return;
}
// If there is no trust settings then the certificate was verified against other trusted certificates already
// or it is self-signed
tce.trustedKeyUsageValue = KnownOIDs.anyExtendedKeyUsage.value();
} else {
List<String> values = new ArrayList<>();
for (var oneTrust : tce.trustSettings) {
Expand Down
37 changes: 35 additions & 2 deletions src/java.base/macosx/native/libosxsecurity/KeystoreImpl.m
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,34 @@ static bool createTrustedCertEntry(JNIEnv *env, jobject keyStore,
return true;
}

static bool validateCertificate(SecCertificateRef certRef) {
SecTrustRef secTrust = NULL;
CFMutableArrayRef subjCerts = CFArrayCreateMutable(NULL, 1, &kCFTypeArrayCallBacks);
CFArraySetValueAtIndex(subjCerts, 0, certRef);

SecPolicyRef policy = SecPolicyCreateBasicX509();
OSStatus ortn = SecTrustCreateWithCertificates(subjCerts, policy, &secTrust);
bool result = false;
if (ortn) {
/* should never happen */
cssmPerror("SecTrustCreateWithCertificates", ortn);
goto errOut;
}

result = SecTrustEvaluateWithError(secTrust, NULL);
errOut:
if (policy) {
CFRelease(policy);
}
if (secTrust) {
timja marked this conversation as resolved.
Show resolved Hide resolved
CFRelease(secTrust);
}
if (subjCerts) {
CFRelease(subjCerts);
}
return result;
}

static void addCertificatesToKeystore(JNIEnv *env, jobject keyStore,
jmethodID jm_createTrustedCertEntry,
jclass jc_arrayListClass,
Expand Down Expand Up @@ -492,9 +520,14 @@ static void addCertificatesToKeystore(JNIEnv *env, jobject keyStore,
goto errOut;
}

// Only add certificates with trust settings
// If no trust settings we need to verify the certificate first
if (inputTrust == NULL) {
timja marked this conversation as resolved.
Show resolved Hide resolved
continue;
bool valid = validateCertificate(certRef);
if (valid) {
inputTrust = (*env)->NewObject(env, jc_arrayListClass, jm_arrayListCons);
} else {
continue;
}
}

// Create java object for certificate with trust settings
Expand Down
3 changes: 2 additions & 1 deletion test/jdk/TEST.groups
Original file line number Diff line number Diff line change
Expand Up @@ -657,7 +657,8 @@ jdk_core_manual_interactive = \
jdk_security_manual_interactive = \
sun/security/tools/keytool/i18n.java \
com/sun/security/auth/callback/TextCallbackHandler/Password.java \
sun/security/krb5/config/native/TestDynamicStore.java
sun/security/krb5/config/native/TestDynamicStore.java \
java/security/KeyStore/CheckMacOSKeyChainIntermediateCATrust.java

# Test sets for running inside container environment
jdk_containers_extended = \
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/*
* Copyright (c) 2025, 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.
*
* 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.
*/

import java.nio.file.Path;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.X509Certificate;
import java.util.Iterator;
import java.util.List;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.stream.StreamSupport;

import jdk.test.lib.process.ProcessTools;

import static org.junit.jupiter.api.Assertions.fail;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/*
* @test
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexeybakhtin quick question on how this should be marked as manual.

I see all tests in https://github.com/openjdk/jdk/blob/master/test/jdk/TEST.groups#L256-L259 are manual ones.
Is this test automatically included in that?

Or should it be added here?
https://github.com/openjdk/jdk/blob/master/test/jdk/TEST.groups#L657

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test should be marked as @run junit/manual and added to the jdk_security_manual_interactive part of the TEST.groups

Copy link
Author

@timja timja Jan 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any idea how I can run the test after making those changes?

The test just gets skipped with:

CONF=release make test TEST=test/jdk/java/security/KeyStore/CheckMacOSKeyChainIntermediateCATrust.java

I've checked through:
https://openjdk.org/jtreg/faq.html#how-do-i-run-only-tests-requiring-manual-intervention

and I saw somewhere mention of passing -manual but I can't get that to work with make test

Copy link
Author

@timja timja Jan 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was able to run it by setting up jtreg and avoiding the make test task.

jtreg.sh -manual test/jdk/java/security/KeyStore/CheckMacOSKeyChainIntermediateCATrust.java

Although it seems it ran anyway without passing manual:

jtreg.sh test/jdk/java/security/KeyStore/CheckMacOSKeyChainIntermediateCATrust.java

Test results: passed: 1

* @bug 8347067
* @library /test/lib
* @requires os.family == "mac"
* @summary Check whether loading of certificates from macOS Keychain correctly
* loads intermediate CA certificates
* @run junit/manual CheckMacOSKeyChainIntermediateCATrust
timja marked this conversation as resolved.
Show resolved Hide resolved
*/
public class CheckMacOSKeyChainIntermediateCATrust {

private static final String DIR = System.getProperty("test.src", ".");
static boolean verbose = false; // avoid too verbose output

@Test
public void test() throws Throwable {
KeyStore ks = KeyStore.getInstance("KeychainStore");
ks.load(null, null);

Iterator<String> iterator = ks.aliases().asIterator();
List<X509Certificate> certificates = StreamSupport.stream(
Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED), false)
.sorted()
.map(alias -> {
try {
return (X509Certificate) ks.getCertificate(alias);
} catch (KeyStoreException e) {
throw new RuntimeException(e);
}
})
.toList();

System.out.println("Verifying expected certificates are trusted");

String rootCASubjectName = "CN=Example CA,O=Example,C=US";
assertThat(containsSubjectName(certificates, rootCASubjectName),
"Root CA not found " + rootCASubjectName, certificates);

String intermediateCASubjectName = "CN=Example Intermediate CA,O=Example,C=US";
assertThat(containsSubjectName(certificates, intermediateCASubjectName),
"Intermediate CA not found " + intermediateCASubjectName, certificates);

String nonTrustedCASubjectName = "CN=Non Trusted Example CA,O=Example,C=US";
assertThat(not(containsSubjectName(certificates, nonTrustedCASubjectName)),
"Non trusted CA found " + nonTrustedCASubjectName, certificates);

String nonTrustedIntermediateCASubjectName = "CN=Non Trusted Example Intermediate CA,O=Example,C=US";
assertThat(not(containsSubjectName(certificates, nonTrustedIntermediateCASubjectName)),
"Non trusted intermediate CA found " + nonTrustedIntermediateCASubjectName, certificates);
}

@BeforeEach
void setup() {
System.out.println("Adding certificates to key chain");
addCertificatesToKeyChain();
}

@AfterEach
void cleanup() {
System.out.println("Cleaning up");
deleteCertificatesFromKeyChain();
}

private static void addCertificatesToKeyChain() {
String loginKeyChain = getLoginKeyChain();

Path caPath = Path.of("%s/%s".formatted(DIR, "test-ca.pem"));
List<String> args = List.of(
"/usr/bin/security",
"add-trusted-cert",
"-k", loginKeyChain,
caPath.toString()
);
executeProcess(args);

caPath = Path.of("%s/%s".formatted(DIR, "non-trusted-test-ca.pem"));
args = List.of(
"/usr/bin/security",
"add-certificates",
"-k", loginKeyChain,
caPath.toString()
);
executeProcess(args);

caPath = Path.of("%s/%s".formatted(DIR, "test-intermediate-ca.pem"));
args = List.of(
"/usr/bin/security",
"add-certificates",
"-k", loginKeyChain,
caPath.toString()
);
executeProcess(args);

}

private static String getLoginKeyChain() {
return Path.of(System.getProperty("user.home"), "Library/Keychains/login.keychain-db").toString();
}

private static void executeProcess(List<String> params) {
System.out.println("Command line: " + params);
try {
ProcessTools.executeProcess(params.toArray(new String[0]))
.shouldHaveExitValue(0);
} catch (Exception e) {
fail("Unexpected exception: " + e);
}
}

private static void deleteCertificatesFromKeyChain() {
executeProcess(
List.of(
"/usr/bin/security",
"delete-certificate",
"-c", "Non Trusted Example CA",
"-t"
)
);

executeProcess(
List.of(
"/usr/bin/security",
"delete-certificate",
"-c", "Example CA",
"-t"
)
);

executeProcess(
List.of(
"/usr/bin/security",
"delete-certificate",
"-c", "Example Intermediate CA",
"-t"
)
);
}

private static boolean not(boolean condition) {
return !condition;
}

private static boolean containsSubjectName(List<X509Certificate> certificates, String subjectName) {
return certificates.stream()
.map(cert -> cert.getSubjectX500Principal().getName())
.anyMatch(name -> name.contains(subjectName));
}

private static List<String> getSubjects(List<X509Certificate> certificates) {
return certificates.stream()
.map(cert -> cert.getSubjectX500Principal().getName())
.toList();
}

private static void assertThat(boolean expected, String message, List<X509Certificate> certificates) {
if (!expected) {
String errMessage = message;
if (verbose) {
errMessage += ", subjects: " + String.join(" | ", getSubjects(certificates));
}
throw new AssertionError(errMessage);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/usr/bin/env bash

set -ex

cd "$(dirname "$0")"

openssl genrsa -out root.key 2048
openssl req -x509 -sha256 -nodes -extensions v3_ca -key root.key -subj "/C=US/O=Example/CN=Example CA" -days 3650 -out test-ca.pem

openssl genrsa -out intermediate.key 2048
openssl req -new -sha256 -nodes -key intermediate.key \
-subj "/C=US/O=Example/CN=Example Intermediate CA" -out test-intermediate-ca.csr

openssl x509 -req \
-extensions v3_ca \
-extfile openssl.cnf \
-in test-intermediate-ca.csr \
-CA test-ca.pem \
-CAkey root.key \
-CAcreateserial \
-out test-intermediate-ca.pem \
-days 3650 \
-sha256

openssl genrsa -out non-trusted-root.key 2048
openssl req -x509 -sha256 -nodes -extensions v3_ca -key non-trusted-root.key -subj "/C=US/O=Example/CN=Non Trusted Example CA" -days 3650 -out non-trusted-test-ca.pem

openssl genrsa -out non-trusted-intermediate.key 2048
openssl req -new -sha256 -nodes -key non-trusted-intermediate.key \
-subj "/C=US/O=Example/CN=Non Trusted Example Intermediate CA" -out non-trusted-intermediate-ca.csr

openssl x509 -req \
-extensions v3_ca \
-extfile openssl.cnf \
-in non-trusted-intermediate-ca.csr \
-CA non-trusted-test-ca.pem \
-CAkey non-trusted-root.key \
-CAcreateserial \
-out non-trusted-intermediate-ca.pem \
-days 3650 \
-sha256

rm -f non-trusted-root.key root.key test-intermediate-ca.csr intermediate.key test-ca.srl non-trusted-intermediate.key \
non-trusted-intermediate-ca.csr non-trusted-test-ca.srl
23 changes: 23 additions & 0 deletions test/jdk/java/security/KeyStore/non-trusted-intermediate-ca.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-----BEGIN CERTIFICATE-----
MIIDyTCCArGgAwIBAgIUFqJJqie2b55pDsftOOBqkWJuX6owDQYJKoZIhvcNAQEL
BQAwQDELMAkGA1UEBhMCVVMxEDAOBgNVBAoMB0V4YW1wbGUxHzAdBgNVBAMMFk5v
biBUcnVzdGVkIEV4YW1wbGUgQ0EwHhcNMjUwMTI3MjEyNDM2WhcNMzUwMTI1MjEy
NDM2WjBNMQswCQYDVQQGEwJVUzEQMA4GA1UECgwHRXhhbXBsZTEsMCoGA1UEAwwj
Tm9uIFRydXN0ZWQgRXhhbXBsZSBJbnRlcm1lZGlhdGUgQ0EwggEiMA0GCSqGSIb3
DQEBAQUAA4IBDwAwggEKAoIBAQCszrhwXVvR5QkSOoIJg0S5Ij1CqAuYf+WS9Ym6
itSzk744j4P2pVd7rM+1SfUfVqY7dFoE7JSQBTw3i/ruo7Zl/USuXFZa93w6g3TB
g0sqA2N4UmNm5Y588Dd2cGIBZgNVvwQzsiG6zWdTppH0rL91hgy32M/CGkVH6WaU
nK4xCe5PiheGkvJBn6OaeqWpG0K76fDWmucUzaMv0b+DmRQ4GsXbwloiKndchGzE
/56EcHNAr1mgvnh+ADTXPKjEsHWv+1cQk4ql4Di53GvVJ/n6eh6K8aGD10OyQlIL
Tk82LI+FxiPvRHF/uBUjndgZsK9k/txDm5VY1z4mpMCd2h1nAgMBAAGjga0wgaow
HQYDVR0OBBYEFIhW8ihXHrc2ywJqt9xZbrcYcNzlMHsGA1UdIwR0MHKAFAy/+rvH
8b9QCumbjj7Q6+9hW9ImoUSkQjBAMQswCQYDVQQGEwJVUzEQMA4GA1UECgwHRXhh
bXBsZTEfMB0GA1UEAwwWTm9uIFRydXN0ZWQgRXhhbXBsZSBDQYIUQWptFePVwo9i
HM6KUT93F4mkQvYwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAfEsL
/d07gw8xFs8hMTvXK/5PKEw4WuxoytrWDfwIUJpWJBOjcCmlZnSmwdZ12PHQ1mB9
oHLr/RZKxYYn2MP9GqDrFTV+wAAAiIw5eU92HQOmPEgXIYhzsvfm29qwR1tdiAQE
4tXc7Y3O2B1b7lcJGbhjAt+/RkDcvT7Pi5jYd2F3apsKPo8GsC0zCsX/t7SnOxhj
qmPsEsBphBfE/dzqkow/iVWPGjvaV2rOrspGOfjF+j+APJNqBuH05uFR7GGaQ2Fm
mhh+nWmmDod/rJpQP5ToTdLeYki9DMsaJjqth1VF5rWUhOzfWczdOFD5szrxxJcV
L+iJJspxFIvQpS3Pgg==
-----END CERTIFICATE-----
21 changes: 21 additions & 0 deletions test/jdk/java/security/KeyStore/non-trusted-test-ca.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDYTCCAkmgAwIBAgIUQWptFePVwo9iHM6KUT93F4mkQvYwDQYJKoZIhvcNAQEL
BQAwQDELMAkGA1UEBhMCVVMxEDAOBgNVBAoMB0V4YW1wbGUxHzAdBgNVBAMMFk5v
biBUcnVzdGVkIEV4YW1wbGUgQ0EwHhcNMjUwMTI3MjEyMzU4WhcNMzUwMTI1MjEy
MzU4WjBAMQswCQYDVQQGEwJVUzEQMA4GA1UECgwHRXhhbXBsZTEfMB0GA1UEAwwW
Tm9uIFRydXN0ZWQgRXhhbXBsZSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
AQoCggEBAKMh6zqtUcvDLwQJTQcKX4XLQ65MrS81OXpzgajZmgd4vgdv/1PtcXkR
Tzqpeyi137BSvn0VM3CM5e4YLHVwg7iarv92v+gEq+sZcErONbdIvHYGp4J9t+1o
YQHfFsGf1juJe6Ey2s4P10FdWLQN+3BMZXwAnaaGCXnYCixs7ocdIpbobUdRLasF
N0NRQ4BZk+vgmqcC69rB66bNUJhkb40mdcqf0aPCpWnd/MHit/NQ/VXyAIEjllZk
i4Jd5aoouOvjI8a7Pp0z+GHFU4RwQjuFfbJvSyeQ5OL1PAn74amwLMnHF+FBJIuW
jwIWmPNDyU9I9WL2YEmi42xhc3R5XIkCAwEAAaNTMFEwHQYDVR0OBBYEFAy/+rvH
8b9QCumbjj7Q6+9hW9ImMB8GA1UdIwQYMBaAFAy/+rvH8b9QCumbjj7Q6+9hW9Im
MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAHouuX960Xzb4M1m
QcNV6m7mAjZDdAlRkuTq3ba1dycH8hJrsT3uE3YNXEKiBIkGCYcX1KXomCgWNOdR
AFc10nV4GV0Com149kmD5oeBp9auqmaSPWTJQxyi7dj/gjNGCfbjf/GD3O1qmfqN
RjKQqluP0koXZjUqL3LJro91XNMDxcQH0CcDdhHv6R7Ob0UndaW1neIljo8U0JFx
yMA+pIua3QppElvynSBSlK5jSDmuVTLrRM401hx/1isKAUmCoz9zuVzjygxt8bdL
gKu66Ze/7RRUee2HxEOxkw53gYfBan+918UXd6feq3nC8A9g6v2L6M6/UkWeTiLs
4KjanI4=
-----END CERTIFICATE-----
17 changes: 17 additions & 0 deletions test/jdk/java/security/KeyStore/openssl.cnf
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[ v3_ca ]


# Extensions for a typical CA


# PKIX recommendation.

subjectKeyIdentifier=hash

authorityKeyIdentifier=keyid:always,issuer:always

# This is what PKIX recommends but some broken software chokes on critical
# extensions.
#basicConstraints = critical,CA:true
# So we do this instead.
basicConstraints = CA:true
Loading