darwinssl: add support for PKCS#12 files for client authentication
authorNick Zitzmann <nickzman@gmail.com>
Thu, 5 Sep 2013 23:57:06 +0000 (18:57 -0500)
committerNick Zitzmann <nickzman@gmail.com>
Thu, 5 Sep 2013 23:57:06 +0000 (18:57 -0500)
I also documented the fact that the OpenSSL engine also supports them.

docs/curl.1
docs/libcurl/curl_easy_setopt.3
lib/curl_darwinssl.c

index a7e2c60..cee54e0 100644 (file)
@@ -394,7 +394,8 @@ If this option is used several times, the last one will be used.
 .IP "-E, --cert <certificate[:password]>"
 (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 <name>"
index 0478fac..f408482 100644 (file)
@@ -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".
index 4ecf2d9..414b7f5 100644 (file)
@@ -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;
     }
   }