8278449: Improve keychain support

Reviewed-by: ahgross, rhalade, mullan
This commit is contained in:
Weijun Wang 2022-01-15 01:22:23 +00:00 committed by Henry Jen
parent 74318dfb9d
commit 9ce2d75d0b
4 changed files with 257 additions and 58 deletions

View File

@ -68,6 +68,25 @@ public final class KeychainStore extends KeyStoreSpi {
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;
};
/**
@ -300,6 +319,35 @@ public final class KeychainStore extends KeyStoreSpi {
}
}
private record LocalAttr(String name, String value)
implements KeyStore.Entry.Attribute {
@Override
public String getName() {
return name;
}
@Override
public String getValue() {
return value;
}
}
@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 tEntry) {
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.
*
@ -453,55 +501,12 @@ public final class KeychainStore extends KeyStoreSpi {
}
/**
* 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 multiple 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.");
}
/**
@ -680,10 +685,7 @@ public final class KeychainStore extends KeyStoreSpi {
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;
@ -778,9 +780,28 @@ public final class KeychainStore extends KeyStoreSpi {
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 {
@ -791,6 +812,69 @@ public final class KeychainStore extends KeyStoreSpi {
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);

View File

@ -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 @@ errOut:
}
}
#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 @@ static void addCertificatesToKeystore(JNIEnv *env, jobject keyStore)
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 @@ static void addCertificatesToKeystore(JNIEnv *env, jobject keyStore)
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 @@ NSString* JavaStringToNSString(JNIEnv *env, jstring jstr) {
/*
* 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)
{

View File

@ -2255,6 +2255,9 @@ public final class Main {
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, ");

View File

@ -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;
@ -277,5 +278,63 @@ public class SecurityTools {
}
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 re) {
throw re;
} 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 re) {
throw re;
} else {
throw new RuntimeException(t);
}
}
}
}
}