From d2fe616e7e44a106ac976aaeaa441ad7d8a6df11 Mon Sep 17 00:00:00 2001 From: Nick Zitzmann Date: Thu, 5 Sep 2013 18:57:06 -0500 Subject: [PATCH] darwinssl: add support for PKCS#12 files for client authentication I also documented the fact that the OpenSSL engine also supports them. --- docs/curl.1 | 10 ++-- docs/libcurl/curl_easy_setopt.3 | 28 +++++------ lib/curl_darwinssl.c | 109 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 124 insertions(+), 23 deletions(-) diff --git a/docs/curl.1 b/docs/curl.1 index a7e2c60..cee54e0 100644 --- a/docs/curl.1 +++ b/docs/curl.1 @@ -394,7 +394,8 @@ If this option is used several times, the last one will be used. .IP "-E, --cert " (SSL) Tells curl to use the specified client certificate file when getting a file with HTTPS, FTPS or another SSL-based protocol. The certificate must be -in PEM format. If the optional password isn't specified, it will be queried +in PKCS#12 format if using Secure Transport, or PEM format if using any other +engine. If the optional password isn't specified, it will be queried for on the terminal. Note that this option assumes a \&"certificate" file that is the private key and the private certificate concatenated! See \fI--cert\fP and \fI--key\fP to specify them independently. @@ -410,9 +411,10 @@ recognized as password delimiter. If the nickname contains "\\", it needs to be escaped as "\\\\" so that it is not recognized as an escape character. (iOS and Mac OS X only) If curl is built against Secure Transport, then the -certificate string must match the name of a certificate that's in the system or -user keychain. The private key corresponding to the certificate, and -certificate chain (if any), must also be present in the keychain. +certificate string can either be the name of a certificate/private key in the +system or user keychain, or the path to a PKCS#12-encoded certificate and +private key. If you want to use a file from the current directory, please +precede it with "./" prefix, in order to avoid confusion with a nickname. If this option is used several times, the last one will be used. .IP "--engine " diff --git a/docs/libcurl/curl_easy_setopt.3 b/docs/libcurl/curl_easy_setopt.3 index 0478fac..f408482 100644 --- a/docs/libcurl/curl_easy_setopt.3 +++ b/docs/libcurl/curl_easy_setopt.3 @@ -2305,22 +2305,20 @@ timeout is set, the internal default of 60000 will be used. (Added in 7.24.0) .SH SSL and SECURITY OPTIONS .IP CURLOPT_SSLCERT Pass a pointer to a zero terminated string as parameter. The string should be -the file name of your certificate. The default format is "PEM" and can be -changed with \fICURLOPT_SSLCERTTYPE\fP. - -With NSS this can also be the nickname of the certificate you wish to -authenticate with. If you want to use a file from the current directory, please -precede it with "./" prefix, in order to avoid confusion with a nickname. - -(iOS and Mac OS X only) With Secure Transport, this string must match the name -of a certificate that's in the system or user keychain. You should encode this -string in UTF-8 format in case it contains non-ASCII characters. The private -key corresponding to the certificate, and certificate chain (if any), must -also be present in the keychain. (Added in 7.31.0) +the file name of your certificate. The default format is "P12" on Secure +Transport and "PEM" on other engines, and can be changed with +\fICURLOPT_SSLCERTTYPE\fP. + +With NSS or Secure Transport, this can also be the nickname of the certificate +you wish to authenticate with as it is named in the security database. If you +want to use a file from the current directory, please precede it with "./" +prefix, in order to avoid confusion with a nickname. .IP CURLOPT_SSLCERTTYPE Pass a pointer to a zero terminated string as parameter. The string should be -the format of your certificate. Supported formats are "PEM" and "DER". (Added -in 7.9.3) +the format of your certificate. Supported formats are "PEM" and "DER", except +with Secure Transport. OpenSSL (versions 0.9.3 and later) and Secure Transport +(on iOS 5 or later, or OS X 10.6 or later) also support "P12" for +PKCS#12-encoded files. (Added in 7.9.3) .IP CURLOPT_SSLKEY Pass a pointer to a zero terminated string as parameter. The string should be the file name of your private key. The default format is "PEM" and can be @@ -2328,7 +2326,7 @@ changed with \fICURLOPT_SSLKEYTYPE\fP. (iOS and Mac OS X only) This option is ignored if curl was built against Secure Transport. Secure Transport expects the private key to be already present in -the keychain containing the certificate. +the keychain or PKCS#12 file containing the certificate. .IP CURLOPT_SSLKEYTYPE Pass a pointer to a zero terminated string as parameter. The string should be the format of your private key. Supported formats are "PEM", "DER" and "ENG". diff --git a/lib/curl_darwinssl.c b/lib/curl_darwinssl.c index 4ecf2d9..414b7f5 100644 --- a/lib/curl_darwinssl.c +++ b/lib/curl_darwinssl.c @@ -819,6 +819,68 @@ static OSStatus CopyIdentityWithLabel(char *label, return status; } +static OSStatus CopyIdentityFromPKCS12File(const char *cPath, + const char *cPassword, + SecIdentityRef *out_cert_and_key) +{ + OSStatus status = errSecItemNotFound; + CFURLRef pkcs_url = CFURLCreateFromFileSystemRepresentation(NULL, + (const UInt8 *)cPath, strlen(cPath), false); + CFStringRef password = cPassword ? CFStringCreateWithCString(NULL, + cPassword, kCFStringEncodingUTF8) : NULL; + CFDataRef pkcs_data = NULL; + + /* We can import P12 files on iOS or OS X 10.6 or later: */ +#if CURL_BUILD_MAC_10_6 || CURL_BUILD_IOS + if(CFURLCreateDataAndPropertiesFromResource(NULL, pkcs_url, &pkcs_data, + NULL, NULL, &status)) { + const void *cKeys[] = {kSecImportExportPassphrase}; + const void *cValues[] = {password}; + CFDictionaryRef options = CFDictionaryCreate(NULL, cKeys, cValues, + password ? 1L : 0L, NULL, NULL); + CFArrayRef items = NULL; + + /* Here we go: */ + status = SecPKCS12Import(pkcs_data, options, &items); + if(status == noErr) { + CFDictionaryRef identity_and_trust = CFArrayGetValueAtIndex(items, 0L); + const void *temp_identity = CFDictionaryGetValue(identity_and_trust, + kSecImportItemIdentity); + + /* Retain the identity; we don't care about any other data... */ + CFRetain(temp_identity); + *out_cert_and_key = (SecIdentityRef)temp_identity; + CFRelease(items); + } + CFRelease(options); + CFRelease(pkcs_data); + } +#endif /* CURL_BUILD_MAC_10_6 || CURL_BUILD_IOS */ + if(password) + CFRelease(password); + CFRelease(pkcs_url); + return status; +} + +/* This code was borrowed from nss.c, with some modifications: + * Determine whether the nickname passed in is a filename that needs to + * be loaded as a PEM or a regular NSS nickname. + * + * returns 1 for a file + * returns 0 for not a file + */ +CF_INLINE bool is_file(const char *filename) +{ + struct_stat st; + + if(filename == NULL) + return false; + + if(stat(filename, &st) == 0) + return S_ISREG(st.st_mode); + return false; +} + static CURLcode darwinssl_connect_step1(struct connectdata *conn, int sockindex) { @@ -988,9 +1050,27 @@ static CURLcode darwinssl_connect_step1(struct connectdata *conn, if(data->set.str[STRING_CERT]) { SecIdentityRef cert_and_key = NULL; + bool is_cert_file = is_file(data->set.str[STRING_CERT]); + + /* User wants to authenticate with a client cert. Look for it: + If we detect that this is a file on disk, then let's load it. + Otherwise, assume that the user wants to use an identity loaded + from the Keychain. */ + if(is_cert_file) { + if(!data->set.str[STRING_CERT_TYPE]) + infof(data, "WARNING: SSL: Certificate type not set, assuming " + "PKCS#12 format.\n"); + else if(strncmp(data->set.str[STRING_CERT_TYPE], "P12", + strlen(data->set.str[STRING_CERT_TYPE])) != 0) + infof(data, "WARNING: SSL: The Security framework only supports " + "loading identities that are in PKCS#12 format.\n"); + + err = CopyIdentityFromPKCS12File(data->set.str[STRING_CERT], + data->set.str[STRING_KEY_PASSWD], &cert_and_key); + } + else + err = CopyIdentityWithLabel(data->set.str[STRING_CERT], &cert_and_key); - /* User wants to authenticate with a client cert. Look for it: */ - err = CopyIdentityWithLabel(data->set.str[STRING_CERT], &cert_and_key); if(err == noErr) { SecCertificateRef cert = NULL; CFTypeRef certs_c[1]; @@ -1027,8 +1107,29 @@ static CURLcode darwinssl_connect_step1(struct connectdata *conn, CFRelease(cert_and_key); } else { - failf(data, "SSL: Can't find the certificate \"%s\" and its private key " - "in the Keychain.", data->set.str[STRING_CERT]); + switch(err) { + case errSecPkcs12VerifyFailure: case errSecAuthFailed: + failf(data, "SSL: Incorrect password for the certificate \"%s\" " + "and its private key.", data->set.str[STRING_CERT]); + break; + case errSecDecode: case errSecUnknownFormat: + failf(data, "SSL: Couldn't make sense of the data in the " + "certificate \"%s\" and its private key.", + data->set.str[STRING_CERT]); + break; + case errSecPassphraseRequired: + failf(data, "SSL The certificate \"%s\" requires a password.", + data->set.str[STRING_CERT]); + break; + case errSecItemNotFound: + failf(data, "SSL: Can't find the certificate \"%s\" and its private " + "key in the Keychain.", data->set.str[STRING_CERT]); + break; + default: + failf(data, "SSL: Can't load the certificate \"%s\" and its private " + "key: OSStatus %d", data->set.str[STRING_CERT], err); + break; + } return CURLE_SSL_CERTPROBLEM; } } -- 2.7.4