8278449: Improve keychain support

Reviewed-by: andrew
Backport-of: 2376bb88eff3ae6922c4cae276e1d703a520853d
diff --git a/src/java.base/macosx/classes/apple/security/KeychainStore.java b/src/java.base/macosx/classes/apple/security/KeychainStore.java
index d4409c4..74a531d 100644
--- a/src/java.base/macosx/classes/apple/security/KeychainStore.java
+++ b/src/java.base/macosx/classes/apple/security/KeychainStore.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011, 2021, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2011, 2022, 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
@@ -68,6 +68,25 @@
 
         Certificate cert;
         long certRef;  // SecCertificateRef for this key
+
+        // Each KeyStore.TrustedCertificateEntry have 2 attributes:
+        // 1. "trustSettings" -> trustSettings.toString()
+        // 2. "2.16.840.1.113894.746875.1.1" -> trustedKeyUsageValue
+        // The 1st one is mainly for debugging use. The 2nd one is similar
+        // to the attribute with the same key in a PKCS12KeyStore.
+
+        // The SecTrustSettingsCopyTrustSettings() output for this certificate
+        // inside the KeyChain in its original array of CFDictionaryRef objects
+        // structure with values dumped as strings. For each trust, an extra
+        // entry "SecPolicyOid" is added whose value is the OID for this trust.
+        // The extra entries are used to construct trustedKeyUsageValue.
+        List<Map<String, String>> trustSettings;
+
+        // One or more OIDs defined in http://oidref.com/1.2.840.113635.100.1.
+        // It can also be "2.5.29.37.0" for a self-signed certificate with
+        // an empty trust settings. This value is never empty. When there are
+        // multiple OID values, it takes the form of "[1.1.1, 1.1.2]".
+        String trustedKeyUsageValue;
     };
 
     /**
@@ -310,6 +329,66 @@
         }
     }
 
+    private final class LocalAttr
+            implements KeyStore.Entry.Attribute {
+
+        private String name;
+        private String value;
+
+        public LocalAttr(String name, String value) {
+            this.name = name;
+            this.value = value;
+        }
+
+        @Override
+        public String getName() {
+            return name;
+        }
+
+        @Override
+        public String getValue() {
+            return value;
+        }
+
+        /**
+         * Calculates a hash code value for the object.
+         * Objects that are equal will also have the same hashcode.
+         */
+        public int hashCode() {
+            return Objects.hash(name, value);
+        }
+
+        public boolean equals(Object obj) {
+            if (this == obj) return true;
+
+            if (!(obj instanceof LocalAttr)) {
+                return false;
+            }
+
+            LocalAttr other =
+                    (LocalAttr) obj;
+            return (Objects.equals(name, other.getName()) &&
+                    Objects.equals(value, other.getValue()));
+        }
+
+    }
+
+    @Override
+    public KeyStore.Entry engineGetEntry(String alias, KeyStore.ProtectionParameter protParam)
+            throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableEntryException {
+        if (engineIsCertificateEntry(alias)) {
+            Object entry = entries.get(alias.toLowerCase());
+            if (entry instanceof TrustedCertEntry) {
+                TrustedCertEntry tEntry = (TrustedCertEntry)entry;
+                return new KeyStore.TrustedCertificateEntry(
+                        tEntry.cert, Set.of(
+                                new LocalAttr(KnownOIDs.ORACLE_TrustedKeyUsage.value(), tEntry.trustedKeyUsageValue),
+                                new LocalAttr("trustSettings", tEntry.trustSettings.toString())));
+            }
+        }
+        return super.engineGetEntry(alias, protParam);
+    }
+
     /**
         * Returns the creation date of the entry identified by the given alias.
      *
@@ -463,55 +542,12 @@
     }
 
     /**
-        * Assigns the given certificate to the given alias.
-     *
-     * <p>If the given alias already exists in this keystore and identifies a
-     * <i>trusted certificate entry</i>, the certificate associated with it is
-     * overridden by the given certificate.
-     *
-     * @param alias the alias name
-     * @param cert the certificate
-     *
-     * @exception KeyStoreException if the given alias already exists and does
-     * not identify a <i>trusted certificate entry</i>, or this operation
-     * fails for some other reason.
+     * Adding trusted certificate entry is not supported.
      */
     public void engineSetCertificateEntry(String alias, Certificate cert)
-        throws KeyStoreException
-    {
-        permissionCheck();
-
-        synchronized(entries) {
-
-            Object entry = entries.get(alias.toLowerCase());
-            if ((entry != null) && (entry instanceof KeyEntry)) {
-                throw new KeyStoreException
-                ("Cannot overwrite key entry with certificate");
-            }
-
-            // This will be slow, but necessary.  Enumerate the values and then see if the cert matches the one in the trusted cert entry.
-            // Security framework doesn't support the same certificate twice in a keychain.
-            Collection<Object> allValues = entries.values();
-
-            for (Object value : allValues) {
-                if (value instanceof TrustedCertEntry) {
-                    TrustedCertEntry tce = (TrustedCertEntry)value;
-                    if (tce.cert.equals(cert)) {
-                        throw new KeyStoreException("Keychain does not support mulitple copies of same certificate.");
-                    }
-                }
-            }
-
-            TrustedCertEntry trustedCertEntry = new TrustedCertEntry();
-            trustedCertEntry.cert = cert;
-            trustedCertEntry.date = new Date();
-            String lowerAlias = alias.toLowerCase();
-            if (entries.get(lowerAlias) != null) {
-                deletedEntries.put(lowerAlias, entries.get(lowerAlias));
-            }
-            entries.put(lowerAlias, trustedCertEntry);
-            addedEntries.put(lowerAlias, trustedCertEntry);
-        }
+            throws KeyStoreException {
+        throw new KeyStoreException("Cannot set trusted certificate entry." +
+                " Use the macOS \"security add-trusted-cert\" command instead.");
     }
 
     /**
@@ -690,10 +726,7 @@
             String alias = e.nextElement();
             Object entry = addedEntries.get(alias);
             if (entry instanceof TrustedCertEntry) {
-                TrustedCertEntry tce = (TrustedCertEntry)entry;
-                Certificate certElem;
-                certElem = tce.cert;
-                tce.certRef = addCertificateToKeychain(alias, certElem);
+                // Cannot set trusted certificate entry
             } else {
                 KeyEntry keyEntry = (KeyEntry)entry;
 
@@ -788,9 +821,28 @@
     private native void _scanKeychain();
 
     /**
-     * Callback method from _scanKeychain.  If a trusted certificate is found, this method will be called.
+     * Callback method from _scanKeychain.  If a trusted certificate is found,
+     * this method will be called.
+     *
+     * inputTrust is a list of strings in groups. Each group contains key/value
+     * pairs for one trust setting and ends with a null. Thus the size of the
+     * whole list is (2 * s_1 + 1) + (2 * s_2 + 1) + ... + (2 * s_n + 1),
+     * where s_i is the size of mapping for the i'th trust setting,
+     * and n is the number of trust settings. Ex:
+     *
+     * key1 for trust1
+     * value1 for trust1
+     * ..
+     * null (end of trust1)
+     * key1 for trust2
+     * value1 for trust2
+     * ...
+     * null (end of trust2)
+     * ...
+     * null (end if trust_n)
      */
-    private void createTrustedCertEntry(String alias, long keychainItemRef, long creationDate, byte[] derStream) {
+    private void createTrustedCertEntry(String alias, List<String> inputTrust,
+            long keychainItemRef, long creationDate, byte[] derStream) {
         TrustedCertEntry tce = new TrustedCertEntry();
 
         try {
@@ -801,6 +853,69 @@
             tce.cert = cert;
             tce.certRef = keychainItemRef;
 
+            tce.trustSettings = new ArrayList<>();
+            Map<String,String> tmpMap = new LinkedHashMap<>();
+            for (int i = 0; i < inputTrust.size(); i++) {
+                if (inputTrust.get(i) == null) {
+                    tce.trustSettings.add(tmpMap);
+                    if (i < inputTrust.size() - 1) {
+                        // Prepare an empty map for the next trust setting.
+                        // Do not just clear(), must be a new object.
+                        // Only create if not at end of list.
+                        tmpMap = new LinkedHashMap<>();
+                    }
+                } else {
+                    tmpMap.put(inputTrust.get(i), inputTrust.get(i+1));
+                    i++;
+                }
+            }
+
+            boolean isSelfSigned;
+            try {
+                cert.verify(cert.getPublicKey());
+                isSelfSigned = true;
+            } catch (Exception e) {
+                isSelfSigned = false;
+            }
+            if (tce.trustSettings.isEmpty()) {
+                if (isSelfSigned) {
+                    // If a self-signed certificate has an empty trust settings,
+                    // trust it for all purposes
+                    tce.trustedKeyUsageValue = KnownOIDs.anyExtendedKeyUsage.value();
+                } else {
+                    // Otherwise, return immediately. The certificate is not
+                    // added into entries.
+                    return;
+                }
+            } else {
+                List<String> values = new ArrayList<>();
+                for (var oneTrust : tce.trustSettings) {
+                    var result = oneTrust.get("kSecTrustSettingsResult");
+                    // https://developer.apple.com/documentation/security/sectrustsettingsresult?language=objc
+                    // 1 = kSecTrustSettingsResultTrustRoot, 2 = kSecTrustSettingsResultTrustAsRoot
+                    // If missing, a default value of kSecTrustSettingsResultTrustRoot is assumed
+                    // for self-signed certificates (see doc for SecTrustSettingsCopyTrustSettings).
+                    // Note that the same SecPolicyOid can appear in multiple trust settings
+                    // for different kSecTrustSettingsAllowedError and/or kSecTrustSettingsPolicyString.
+                    if ((result == null && isSelfSigned)
+                            || "1".equals(result) || "2".equals(result)) {
+                        // When no kSecTrustSettingsPolicy, it means everything
+                        String oid = oneTrust.getOrDefault("SecPolicyOid",
+                                KnownOIDs.anyExtendedKeyUsage.value());
+                        if (!values.contains(oid)) {
+                            values.add(oid);
+                        }
+                    }
+                }
+                if (values.isEmpty()) {
+                    return;
+                }
+                if (values.size() == 1) {
+                    tce.trustedKeyUsageValue = values.get(0);
+                } else {
+                    tce.trustedKeyUsageValue = values.toString();
+                }
+            }
             // Make a creation date.
             if (creationDate != 0)
                 tce.date = new Date(creationDate);
diff --git a/src/java.base/macosx/native/libosxsecurity/KeystoreImpl.m b/src/java.base/macosx/native/libosxsecurity/KeystoreImpl.m
index f427737..3f0c1de 100644
--- a/src/java.base/macosx/native/libosxsecurity/KeystoreImpl.m
+++ b/src/java.base/macosx/native/libosxsecurity/KeystoreImpl.m
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011, 2021, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2011, 2022, 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
@@ -368,6 +368,14 @@
     }
 }
 
+#define ADD(list, str) { \
+    jobject localeObj = (*env)->NewStringUTF(env, [str UTF8String]); \
+    (*env)->CallBooleanMethod(env, list, jm_listAdd, localeObj); \
+    (*env)->DeleteLocalRef(env, localeObj); \
+}
+
+#define ADDNULL(list) (*env)->CallBooleanMethod(env, list, jm_listAdd, NULL)
+
 static void addCertificatesToKeystore(JNIEnv *env, jobject keyStore)
 {
     // Search the user keychain list for all X509 certificates.
@@ -379,8 +387,15 @@
     jclass jc_KeychainStore = (*env)->FindClass(env, "apple/security/KeychainStore");
     CHECK_NULL(jc_KeychainStore);
     jmethodID jm_createTrustedCertEntry = (*env)->GetMethodID(
-            env, jc_KeychainStore, "createTrustedCertEntry", "(Ljava/lang/String;JJ[B)V");
+            env, jc_KeychainStore, "createTrustedCertEntry", "(Ljava/lang/String;Ljava/util/List;JJ[B)V");
     CHECK_NULL(jm_createTrustedCertEntry);
+    jclass jc_arrayListClass = (*env)->FindClass(env, "java/util/ArrayList");
+    CHECK_NULL(jc_arrayListClass);
+    jmethodID jm_arrayListCons = (*env)->GetMethodID(env, jc_arrayListClass, "<init>", "()V");
+    CHECK_NULL(jm_arrayListCons);
+    jmethodID jm_listAdd = (*env)->GetMethodID(env, jc_arrayListClass, "add", "(Ljava/lang/Object;)Z");
+    CHECK_NULL(jm_listAdd);
+
     do {
         searchResult = SecKeychainSearchCopyNext(keychainItemSearch, &theItem);
 
@@ -401,12 +416,50 @@
                 goto errOut;
             }
 
+            // Only add certificates with trusted settings
+            CFArrayRef trustSettings;
+            if (SecTrustSettingsCopyTrustSettings(certRef, kSecTrustSettingsDomainUser, &trustSettings)
+                    == errSecItemNotFound) {
+                continue;
+            }
+
+            // See KeychainStore::createTrustedCertEntry for content of inputTrust
+            jobject inputTrust = (*env)->NewObject(env, jc_arrayListClass, jm_arrayListCons);
+            CHECK_NULL(inputTrust);
+
+            // Dump everything inside trustSettings into inputTrust
+            CFIndex count = CFArrayGetCount(trustSettings);
+            for (int i = 0; i < count; i++) {
+                CFDictionaryRef oneTrust = (CFDictionaryRef) CFArrayGetValueAtIndex(trustSettings, i);
+                CFIndex size = CFDictionaryGetCount(oneTrust);
+                const void * keys [size];
+                const void * values [size];
+                CFDictionaryGetKeysAndValues(oneTrust, keys, values);
+                for (int j = 0; j < size; j++) {
+                    NSString* s = [NSString stringWithFormat:@"%@", keys[j]];
+                    ADD(inputTrust, s);
+                    s = [NSString stringWithFormat:@"%@", values[j]];
+                    ADD(inputTrust, s);
+                }
+                SecPolicyRef certPolicy;
+                certPolicy = (SecPolicyRef)CFDictionaryGetValue(oneTrust, kSecTrustSettingsPolicy);
+                if (certPolicy != NULL) {
+                    CFDictionaryRef policyDict = SecPolicyCopyProperties(certPolicy);
+                    ADD(inputTrust, @"SecPolicyOid");
+                    NSString* s = [NSString stringWithFormat:@"%@", CFDictionaryGetValue(policyDict, @"SecPolicyOid")];
+                    ADD(inputTrust, s);
+                    CFRelease(policyDict);
+                }
+                ADDNULL(inputTrust);
+            }
+            CFRelease(trustSettings);
+
             // Find the creation date.
             jlong creationDate = getModDateFromItem(env, theItem);
 
             // Call back to the Java object to create Java objects corresponding to this security object.
             jlong nativeRef = ptr_to_jlong(certRef);
-            (*env)->CallVoidMethod(env, keyStore, jm_createTrustedCertEntry, alias, nativeRef, creationDate, certData);
+            (*env)->CallVoidMethod(env, keyStore, jm_createTrustedCertEntry, alias, inputTrust, nativeRef, creationDate, certData);
             JNU_CHECK_EXCEPTION(env);
         }
     } while (searchResult == noErr);
@@ -522,8 +575,8 @@
 /*
  * Class:     apple_security_KeychainStore
  * Method:    _addItemToKeychain
- * Signature: (Ljava/lang/String;[B)I
-*/
+ * Signature: (Ljava/lang/String;Z[B[C)J
+ */
 JNIEXPORT jlong JNICALL Java_apple_security_KeychainStore__1addItemToKeychain
 (JNIEnv *env, jobject this, jstring alias, jboolean isCertificate, jbyteArray rawDataObj, jcharArray passwordObj)
 {
diff --git a/src/java.base/share/classes/sun/security/tools/keytool/Main.java b/src/java.base/share/classes/sun/security/tools/keytool/Main.java
index 4499d90..3582ca5 100644
--- a/src/java.base/share/classes/sun/security/tools/keytool/Main.java
+++ b/src/java.base/share/classes/sun/security/tools/keytool/Main.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1997, 2021, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997, 2022, 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
@@ -2150,6 +2150,9 @@
                 out.println(mf);
                 dumpCert(cert, out);
             } else if (debug) {
+                for (var attr : keyStore.getEntry(alias, null).getAttributes()) {
+                    System.out.println("Attribute " + attr.getName() + ": " + attr.getValue());
+                }
                 out.println(cert.toString());
             } else {
                 out.println("trustedCertEntry, ");
diff --git a/test/lib/jdk/test/lib/SecurityTools.java b/test/lib/jdk/test/lib/SecurityTools.java
index 30da87f..0e4865c 100644
--- a/test/lib/jdk/test/lib/SecurityTools.java
+++ b/test/lib/jdk/test/lib/SecurityTools.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2022, 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
@@ -23,6 +23,7 @@
 
 package jdk.test.lib;
 
+import java.io.Closeable;
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
@@ -275,5 +276,63 @@
         }
         return result;
     }
+
+    // Create a temporary keychain in macOS and use it. The original
+    // keychains will be restored when the object is closed.
+    public static class TemporaryKeychain implements Closeable {
+        // name of new keychain
+        private final String newChain;
+        // names of the original keychains
+        private final List<String> oldChains;
+
+        public TemporaryKeychain(String name) {
+            Path p = Path.of(name + ".keychain-db");
+            newChain = p.toAbsolutePath().toString();
+            try {
+                oldChains = ProcessTools.executeProcess("security", "list-keychains")
+                        .shouldHaveExitValue(0)
+                        .getStdout()
+                        .lines()
+                        .map(String::trim)
+                        .map(x -> x.startsWith("\"") ? x.substring(1, x.length() - 1) : x)
+                        .collect(Collectors.toList());
+                if (!Files.exists(p)) {
+                    ProcessTools.executeProcess("security", "create-keychain", "-p", "changeit", newChain)
+                            .shouldHaveExitValue(0);
+                }
+                ProcessTools.executeProcess("security", "unlock-keychain", "-p", "changeit", newChain)
+                        .shouldHaveExitValue(0);
+                ProcessTools.executeProcess("security", "list-keychains", "-s", newChain)
+                        .shouldHaveExitValue(0);
+            } catch (Throwable t) {
+                if (t instanceof RuntimeException) {
+                    throw (RuntimeException)t;
+                } else {
+                    throw new RuntimeException(t);
+                }
+            }
+        }
+
+        public String chain() {
+            return newChain;
+        }
+
+        @Override
+        public void close() throws IOException {
+            List<String> cmds = new ArrayList<>();
+            cmds.addAll(List.of("security", "list-keychains", "-s"));
+            cmds.addAll(oldChains);
+            try {
+                ProcessTools.executeProcess(cmds.toArray(new String[0]))
+                        .shouldHaveExitValue(0);
+            } catch (Throwable t) {
+                if (t instanceof RuntimeException) {
+                    throw (RuntimeException)t;
+                } else {
+                    throw new RuntimeException(t);
+                }
+            }
+        }
+    }
 }