Skip to content

Commit

Permalink
Revert private keychain use in the Security Update Checker when run a…
Browse files Browse the repository at this point in the history
…s root on macOS, in order to avoid changing the default System Keychain. Closes GH-1922. Remove Cert and Key from keychain separately, to avoid errors when clearing the client certificate.

libcurl Keychain Problem, Solution, and Code

Overview of the general Problem:

The original problem is that on macOS libcurl doesn’t load the client certificate in such a way that it has permission to use it without asking the user first. This causes a popup which we want to prevent.

Overview of the general Solution:

I work around the problem by loading the client certificate into the keychain with the correct access permissions before we call curl, and then removing it afterwards.

History of approaches taken:
Initially I loaded the cert and removed the certificate each using a single call to the keychain services api.
Then I found that when passenger was run as root like when run under the system apache, that we got errors when I tried to remove the certificate/key pair that led me to believe there was a permissions issue.
In response to that I created a private keychain and added the certificate to it when I detected that the system keychain was the default keychain for the user Passenger is running as. I then set the private keychain as the default keychain for the user (curl only uses the default keychain, it doesn't allow you to set a specific keychain to use). I then reset the keychain to the old default on passenger shutdown.
However it turns out that even if you reset the default keychain back to what it was, macOS does not fully revert the changes to /Library/Preferences/com.apple.security.plist which in turn breaks things like time machine and automatically joining wifi networks.
So I ditched the private keychain approach and went back and researched the error I got when I removed the certificate/key pair from the keychain, and found that if I keep a copy of the random label given to the private key when it is added to the keychain, that I can remove the key and certificate separately and not get the same error that I was getting.

Code:
The code for this resides entirely in SecurityUpdateChecker.h, Crypto.h, and Crypto.cpp

The relevant code in SecurityUpdateChecker.h is the call to `crypto->preAuthKey(clientCertPath.c_str(), CLIENT_CERT_PWD, CLIENT_CERT_LABEL)` in `prepareCurlPOST()` and the call to `crypto->killKey(CLIENT_CERT_LABEL);` in `checkAndLogSecurityUpdate()`. These functions are responsible for adding the certificate/key pair to the keychain and then removing it again after lib curl is done.

While I was using the private keychain approach, there was a bit more code to create the private keychain, and keep track of the old default keychain and whether or not the private keychain was in use, but it has been removed now that this approach was been abandoned.

The Crypto.h header code is just there to make things compile.

Crypto.cpp is where all the interesting code is:

The constructor initializes the variable that holds the private key's random label: id to NULL.

`preAuthKey` checks if the certificate/key pair is already in the keychain using `lookupKeychainItem`, and if so issues an error and returns an unsuccessful status because we prefer writing a warning and skipping the update check to causing a keychain popup. If the cert/key pair is not found then we disable popups (disabling popups and doing nothing more causes libcurl to crash, so it's not a valid approach to the the general problem), load the certificate by calling `copyIdentityFromPKCS12File`, then re-enable popups (because according to the documentation this is a system wide flag and we don't want to break other software, especially since  we know anything that uses libcurl is liable to blow up). After each call to a keychain services api, we check for errors and log them as needed.

`lookupKeychainItem` calls `createQueryDict` to receive a query to find our certificate/key pair if it is in the keychain. Then calls `SecItemCopyMatching` which is the keychain services API for searching the keychain. It returns whether or not the certificate was found and if so sets the second argument pointer to point to it.

`createQueryDict` simply builds a dictionary that contains the key value pairs to uniquely identify our cert/key pair and specify that we want a reference to it, if found.

`copyIdentityFromPKCS12File` is where the certificate is actually loaded. We begin by reading in the p12 file's data, and creating variables of the correct types for use with the keychain services API. Then if the file is read in correctly we call `createAccess` which returns an access struct that allows us to use the certificate after it's loaded (this is what's missing from libcurl) then after packing the access struct and password into a dictionary we pass that and the p12 file's data to `SecPKCS12Import` which does the actual loading of the certificate/key pair. Then if that succeeds we record the random label that the keychain assigned to the private key in the `id` variable and cleanup.

`createAccess` simply wraps the call to `SecAccessCreate` to move some type wrangling out of an already huge function, `SecAccessCreate ` creates the actual access struct.

`killKey` checks if the cert/key pair is found using `lookupKeychainItem`, and if so builds a dictionary to target the certificate then calls the SecItemDelete api to delete the cert, and then if a key label is present, does the same for the key too and clears the `id` variable.
  • Loading branch information
CamJN committed Apr 10, 2017
1 parent 465f7ba commit 236eacd
Show file tree
Hide file tree
Showing 4 changed files with 31 additions and 118 deletions.
1 change: 1 addition & 0 deletions CHANGELOG
Expand Up @@ -8,6 +8,7 @@ Next version (not yet released)
* [Nginx] The preferred Nginx version is now 1.10.3 (previously 1.10.2).
* Adds Ubuntu 17.04 "Zesty" packages.
* Added additional debug level logging for troubleshooting issues with bash scripts. Closes GH-1928.
* Revert private keychain use in the Security Update Checker when run as root on macOS, in order to avoid changing the default System Keychain. Closes GH-1922. Remove Cert and Key from keychain separately, to avoid errors when clearing the client certificate.


Release 5.1.2
Expand Down
118 changes: 4 additions & 114 deletions src/agent/Core/SecurityUpdateChecker.h
Expand Up @@ -59,11 +59,6 @@ class SecurityUpdateChecker {
string serverVersion;
CurlProxyInfo proxyInfo;
Crypto *crypto;
#if BOOST_OS_MACOS
SecKeychainRef defaultKeychain;
SecKeychainRef keychain;
bool usingPassengerKeychain;
#endif

void threadMain() {
TRACE_POINT();
Expand Down Expand Up @@ -227,8 +222,8 @@ class SecurityUpdateChecker {
}

#if BOOST_OS_MACOS
// if not using a private keychain, preauth the security update check key in the user's keychain (this is for libcurl's benefit because they don't bother to authorize themselves to use the keys they import)
if (!usingPassengerKeychain && !crypto->preAuthKey(clientCertPath.c_str(), CLIENT_CERT_PWD, CLIENT_CERT_LABEL)) {
// preauth the security update check key in the user's keychain (this is for libcurl's benefit because they don't bother to authorize themselves to use the keys they import)
if (!crypto->preAuthKey(clientCertPath.c_str(), CLIENT_CERT_PWD, CLIENT_CERT_LABEL)) {
return CURLE_SSL_CERTPROBLEM;
}
if (CURLE_OK != (code = curl_easy_setopt(curl, CURLOPT_SSLCERTTYPE, "P12"))) {
Expand Down Expand Up @@ -298,83 +293,6 @@ class SecurityUpdateChecker {
checkIntervalSec = 0;
#if BOOST_OS_MACOS
clientCertPath = locator.getResourcesDir() + "/update_check_client_cert.p12";
// Used to keep track of which approach we are using, false means we are preauthing the key in the running user's own keychain; true means we create a private keychain and set it as the default
usingPassengerKeychain = false;
defaultKeychain = NULL;
keychain = NULL;
OSStatus status = 0;
char pathName [PATH_MAX];
UInt32 length = PATH_MAX;
memset(pathName, 0, PATH_MAX);

status = SecKeychainCopyDefault(&defaultKeychain);
if (status) {
CFStringRef str = SecCopyErrorMessageString(status, NULL);
P_ERROR(string("Getting default keychain failed: ") +
CFStringGetCStringPtr(str, kCFStringEncodingUTF8) +
" Passenger will not attempt to create a private keychain.");
CFRelease(str);
} else {
status = SecKeychainGetPath(defaultKeychain, &length, pathName);
P_DEBUG(string("username is: ") + getProcessUsername());
if (status) {
CFStringRef str = SecCopyErrorMessageString(status, NULL);
P_ERROR(string("Checking default keychain path failed: ") +
CFStringGetCStringPtr(str, kCFStringEncodingUTF8) +
" Passenger may use system keychain.");
CFRelease(str);
pathName[0] = 0; // ensure the pathName compares cleanly
} else {
P_DEBUG(string("Old default keychain is: ") + pathName);
}
}
// we don't care so much about which user we are, what we care about is is they have their own keychain, if the default keychain is the system keychain, then we need to try and create our own to avoid permissions issues
if (strcmp(pathName, "/Library/Keychains/System.keychain") == 0) {
usingPassengerKeychain = true;
const uint size = 512;
uint8_t keychainPassword[size];
if (!crypto->generateRandomChars(keychainPassword, size)) {
P_CRITICAL("Creating password for Passenger default keychain failed.");
usingPassengerKeychain = false;
} else {
string keychainDir = instancePath;
if (instancePath.length() == 0) {
char currentPath[PATH_MAX];
if (!getcwd(currentPath, PATH_MAX)) {
P_ERROR(string("Failed to get cwd: ") + strerror(errno) + " Attempting to use relative path '.'");
keychainDir = ".";
} else {
keychainDir = string(currentPath);
}
}
// create keychain with long random password, then discard password after creation. We receive the keychain unlocked, and no-one else needs to access the keychain.
status = SecKeychainCreate((keychainDir + "/passenger.keychain").c_str(), size, keychainPassword, false, NULL, &keychain);
memset(keychainPassword, 0, size);
if (status) {
CFStringRef str = SecCopyErrorMessageString(status, NULL);
P_ERROR(string("Creating Passenger default keychain failed: ") +
CFStringGetCStringPtr(str, kCFStringEncodingUTF8) +
" Passenger may fail to access system keychain.");
CFRelease(str);
usingPassengerKeychain = false;
} else {
// set keychain as default so libcurl uses it.
status = SecKeychainSetDefault(keychain);
if (status) {
CFStringRef str = SecCopyErrorMessageString(status, NULL);
P_ERROR(string("Setting Passenger default keychain failed: ") +
CFStringGetCStringPtr(str, kCFStringEncodingUTF8) +
" Passenger may fail to access system keychain.");
CFRelease(str);
usingPassengerKeychain = false;
} else if (!crypto->preAuthKey(clientCertPath.c_str(), CLIENT_CERT_PWD, CLIENT_CERT_LABEL)) {
P_ERROR("Failed to preauthorize Passenger Client Cert, you may experience popups from the Keychain.");
/* } else {
we have loaded the security update check key into the private keychain with the correct permissions, so libcurl should be able to use it. */
}
}
}
}
#else
clientCertPath = locator.getResourcesDir() + "/update_check_client_cert.pem";
#endif
Expand All @@ -400,32 +318,6 @@ class SecurityUpdateChecker {
if (crypto) {
delete crypto;
}
#if BOOST_OS_MACOS
// if using a private keychain, cleanup keychain on shutdown
if (usingPassengerKeychain) {
OSStatus status = 0;
if (defaultKeychain) {
status = SecKeychainSetDefault(defaultKeychain);
if (status) {
CFStringRef str = SecCopyErrorMessageString(status, NULL);
P_ERROR(string("Restoring default keychain failed: ") +
CFStringGetCStringPtr(str, kCFStringEncodingUTF8));
CFRelease(str);
}
CFRelease(defaultKeychain);
}
if (keychain) {
status = SecKeychainDelete(keychain);
if (status) {
CFStringRef str = SecCopyErrorMessageString(status, NULL);
P_ERROR(string("Deleting Passenger private keychain failed: ") +
CFStringGetCStringPtr(str, kCFStringEncodingUTF8));
CFRelease(str);
}
CFRelease(keychain);
}
}
#endif
}

/**
Expand Down Expand Up @@ -658,10 +550,8 @@ class SecurityUpdateChecker {
} while (0);

#if BOOST_OS_MACOS
// if not using a private keychain remove the security update check key from the user's keychain so that if we are stopped/crash and are upgraded or reinstalled before restarting we don't have permission problems
if (!usingPassengerKeychain) {
crypto->killKey(CLIENT_CERT_LABEL);
}
// remove the security update check key from the user's keychain so that if we are stopped/crash and are upgraded or reinstalled before restarting we don't have permission problems
crypto->killKey(CLIENT_CERT_LABEL);
#endif

if (signatureChars) {
Expand Down
29 changes: 25 additions & 4 deletions src/cxx_supportlib/Crypto.cpp
Expand Up @@ -47,7 +47,8 @@ using namespace oxt;

#if BOOST_OS_MACOS

Crypto::Crypto() {
Crypto::Crypto()
:id(NULL) {
}

Crypto::~Crypto() {
Expand Down Expand Up @@ -139,6 +140,9 @@ OSStatus Crypto::copyIdentityFromPKCS12File(const char *cPath,
logError( prefix + ": " + CFStringGetCStringPtr(str, kCFStringEncodingUTF8) + "\n" + suffix );
CFRelease(str);
}
}else{
id = (CFDataRef)CFDictionaryGetValue((CFDictionaryRef)CFArrayGetValueAtIndex(items, 0),kSecImportItemKeyID);
CFRetain(id);
}

if (items) {
Expand All @@ -165,18 +169,35 @@ void Crypto::killKey(const char *cLabel) {

CFArrayRef itemList = CFArrayCreate(NULL, (const void **) &id, 1, NULL);
CFTypeRef keys[] = { kSecClass, kSecMatchItemList, kSecMatchLimit };
CFTypeRef values[] = { kSecClassIdentity, itemList, kSecMatchLimitAll };
CFTypeRef values[] = { kSecClassCertificate, itemList, kSecMatchLimitOne };

CFDictionaryRef dict = CFDictionaryCreate(NULL, keys, values, 3L, NULL, NULL);
OSStatus oserr = SecItemDelete(dict);
if (oserr) {
CFStringRef str = SecCopyErrorMessageString(oserr, NULL);
logError(string("Removing Passenger Cert from keychain failed: ") + CFStringGetCStringPtr(str, kCFStringEncodingUTF8) +
" Please remove the private key from the certificate labeled " + cLabel + " in your keychain.");
" Please remove the certificate labeled " + cLabel + " in your keychain.");
CFRelease(str);
}
CFRelease(dict);
CFRelease(itemList);

if(id){
CFTypeRef keys2[] = { kSecClass, kSecAttrSubjectKeyID, kSecMatchLimit };
CFTypeRef values2[] = { kSecClassKey, id, kSecMatchLimitOne };
dict = CFDictionaryCreate(NULL, keys2, values2, 3L, NULL, NULL);
oserr = SecItemDelete(dict);
if (oserr) {
CFStringRef str = SecCopyErrorMessageString(oserr, NULL);
logError(string("Removing Passenger private key from keychain failed: ") + CFStringGetCStringPtr(str, kCFStringEncodingUTF8) +
" Please remove the private key from the certificate labeled " + cLabel + " in your keychain.");
CFRelease(str);
}
CFRelease(dict);
CFRelease(id);
id = NULL;
}

} else {
CFStringRef str = SecCopyErrorMessageString(status, NULL);
logError(string("Finding Passenger Cert failed: ") + CFStringGetCStringPtr(str, kCFStringEncodingUTF8) );
Expand Down Expand Up @@ -210,7 +231,7 @@ bool Crypto::preAuthKey(const char *path, const char *passwd, const char *cLabel
}
return success;
} else {
logError(string("Passenger client certificate was found in the keychain unexpectedly, you may see keychain popups until you remove the private key from the certificate labeled ") + cLabel + " in your keychain.");
logError(string("Passenger client certificate was found in the keychain unexpectedly, skipping security update check. Please remove the private key from the certificate labeled ") + cLabel + " in your keychain.");
if (id) {
CFRelease(id);
}
Expand Down
1 change: 1 addition & 0 deletions src/cxx_supportlib/Crypto.h
Expand Up @@ -80,6 +80,7 @@ class Crypto {
* log prefix using P_ERROR, and (library-specific) detail from either additional or global query
*/
#if BOOST_OS_MACOS
CFDataRef id;
// (additional needs to be defined as a CFErrorRef, void * won't work)
void logFreeErrorExtended(const StaticString &prefix, CFErrorRef &additional);
CFDictionaryRef createQueryDict(const char *label);
Expand Down

0 comments on commit 236eacd

Please sign in to comment.