Import pkcs12_parse() function from GnuTLS to fix PKCS#12 handling
authorDavid Woodhouse <David.Woodhouse@intel.com>
Thu, 31 May 2012 14:07:31 +0000 (15:07 +0100)
committerDavid Woodhouse <David.Woodhouse@intel.com>
Thu, 31 May 2012 14:39:06 +0000 (15:39 +0100)
An immediate effect is that this fixes the checking of cert expiry for
PKCS#12 certificates.

But it also means we can include the full supporting chain of
intermediate CAs (which has to be pre-assembled before we ever call
gnutls_certificate_set_x509_key() and can't be appended later), and we
can use the extra certs from the PKCS#12 file too, which parse_pkcs12()
currently doesn't bother to give us.

The plan is to fix parse_pkcs12(), submit the changes back upstream and
make it an exported function there, then stick a version-conditional on
our local copy and look forward to the day when we can rip it out again.

Signed-off-by: David Woodhouse <David.Woodhouse@intel.com>
gnutls.c
gnutls_pkcs12.c [new file with mode: 0644]

index 3e6bec5..2c5891b 100644 (file)
--- a/gnutls.c
+++ b/gnutls.c
@@ -310,12 +310,18 @@ static int load_datum(struct openconnect_info *vpninfo,
        return 0;
 }
 
+/* Pull in our local copy of GnuTLS's parse_pkcs12() function, for now */
+#include "gnutls_pkcs12.c"
+
 /* A non-zero, non-error return to make load_certificate() continue and
    interpreting the file as other types */
 #define NOT_PKCS12     1
 
 static int load_pkcs12_certificate(struct openconnect_info *vpninfo,
-                                  gnutls_datum_t *datum)
+                                  gnutls_datum_t *datum,
+                                  gnutls_x509_privkey_t *key,
+                                  gnutls_x509_crt_t *cert,
+                                  gnutls_x509_crl_t * crl)
 {
        gnutls_pkcs12_t p12;
        char *pass;
@@ -377,28 +383,23 @@ static int load_pkcs12_certificate(struct openconnect_info *vpninfo,
                return ret;
        }
 
-       /* We can't actually *use* this gnutls_pkcs12_t, AFAICT.
-          We have to let GnuTLS re-import it all again. */
+       err = parse_pkcs12(vpninfo->https_cred, p12, pass, key, cert, crl);
        gnutls_pkcs12_deinit(p12);
-
-       err = gnutls_certificate_set_x509_simple_pkcs12_mem(vpninfo->https_cred, datum,
-                                                           GNUTLS_X509_FMT_DER, pass);
-               
        if (err) {
                vpn_progress(vpninfo, PRG_ERR,
                             _("Failed to load PKCS#12 certificate: %s\n"),
                             gnutls_strerror(err));
                return -EINVAL;
        }
-       /* FIXME: We haven't checked the certificate expiry */
        return 0;
 }
 
 static int load_certificate(struct openconnect_info *vpninfo)
 {
        gnutls_datum_t fdata;
-       gnutls_x509_crt_t cert;
-       gnutls_x509_privkey_t key;
+       gnutls_x509_privkey_t key = NULL;
+       gnutls_x509_crt_t cert = NULL;
+       gnutls_x509_crl_t crl = NULL;
        int err;
 
        if (vpninfo->cert_type == CERT_TYPE_TPM) {
@@ -433,13 +434,15 @@ static int load_certificate(struct openconnect_info *vpninfo)
 
        if (vpninfo->cert_type == CERT_TYPE_PKCS12 ||
            vpninfo->cert_type == CERT_TYPE_UNKNOWN) {
-               err = load_pkcs12_certificate(vpninfo, &fdata);
-               /* Either it's printed and error and failed, or it's succeeded */
-               if (err <= 0) {
+               err = load_pkcs12_certificate(vpninfo, &fdata, &key, &cert, &crl);
+               if (!err)
+                       goto got_cert;
+               else if (err <= 0) {
                        gnutls_free(fdata.data);
                        return err;
                }
-               /* ... or it falls through to try PEM formats */
+               /* It returned NOT_PKCS12.
+                  Fall through to try PEM formats. */
        }
 
        gnutls_x509_crt_init(&cert);
@@ -506,14 +509,28 @@ static int load_certificate(struct openconnect_info *vpninfo)
                        }
                }
        }
+ got_cert:
+       gnutls_free(fdata.data);
+       check_certificate_expiry(vpninfo, cert);
+
+       if (crl) {
+               err = gnutls_certificate_set_x509_crl(vpninfo->https_cred, &crl, 1);
+               gnutls_x509_crl_deinit(crl);
+               if (err) {
+                       vpn_progress(vpninfo, PRG_ERR,
+                                    _("Setting certificate recovation list failed: %s\n"),
+                                    gnutls_strerror(err));
+                       gnutls_x509_privkey_deinit(key);
+                       gnutls_x509_crt_deinit(cert);
+                       return -EIO;
+               }
+       }
        /* FIXME: We need to work around OpenSSL RT#1942 on the server, by including
           as much of the chain of issuer certificates as we can. */
        err = gnutls_certificate_set_x509_key(vpninfo->https_cred,
                                              &cert, 1, key);
        gnutls_x509_privkey_deinit(key);
-       check_certificate_expiry(vpninfo, cert);
        gnutls_x509_crt_deinit(cert);
-       gnutls_free(fdata.data);
        if (err) {
                vpn_progress(vpninfo, PRG_ERR,
                             _("Setting certificate failed: %s\n"),
diff --git a/gnutls_pkcs12.c b/gnutls_pkcs12.c
new file mode 100644 (file)
index 0000000..623f47c
--- /dev/null
@@ -0,0 +1,341 @@
+/*
+ * Ick. This is (or at least started off as) a straight copy of
+ * parse_pkcs12() from GnuTLS lib/gnutls_x509.c, as of commit ID
+ * 77670476814c078bbad56ce8772b192a3b5736b6 on the gnutls_2_12_x
+ * branch.
+ *
+ * We need to *see* the cert so that we can check its expiry, and
+ * we'll also want to get all the other certs in the PKCS#12 file
+ * rather than only the leaf node. Hopefully these changes can be
+ * merged back into GnuTLS as soon as possible, it can be made a
+ * public function, and this copy can die.
+ */
+#define opaque unsigned char
+#define gnutls_assert() do {} while(0)
+
+/*
+ * Copyright (C) 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010
+ * Free Software Foundation, Inc.
+ *
+ * Author: Nikos Mavrogiannopoulos
+ *
+ * This file WAS part of GnuTLS.
+ *
+ * The GnuTLS is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+ * USA
+ *
+ */
+
+
+static int
+parse_pkcs12 (gnutls_certificate_credentials_t res,
+              gnutls_pkcs12_t p12,
+              const char *password,
+              gnutls_x509_privkey_t * key,
+              gnutls_x509_crt_t * cert, gnutls_x509_crl_t * crl)
+{
+  gnutls_pkcs12_bag_t bag = NULL;
+  int idx = 0;
+  int ret;
+  size_t cert_id_size = 0;
+  size_t key_id_size = 0;
+  opaque cert_id[20];
+  opaque key_id[20];
+  int privkey_ok = 0;
+
+  *cert = NULL;
+  *key = NULL;
+  *crl = NULL;
+
+  /* find the first private key */
+  for (;;)
+    {
+      int elements_in_bag;
+      int i;
+
+      ret = gnutls_pkcs12_bag_init (&bag);
+      if (ret < 0)
+        {
+          bag = NULL;
+          gnutls_assert ();
+          goto done;
+        }
+
+      ret = gnutls_pkcs12_get_bag (p12, idx, bag);
+      if (ret == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE)
+        break;
+      if (ret < 0)
+        {
+          gnutls_assert ();
+          goto done;
+        }
+
+      ret = gnutls_pkcs12_bag_get_type (bag, 0);
+      if (ret < 0)
+        {
+          gnutls_assert ();
+          goto done;
+        }
+
+      if (ret == GNUTLS_BAG_ENCRYPTED)
+        {
+          ret = gnutls_pkcs12_bag_decrypt (bag, password);
+          if (ret < 0)
+            {
+              gnutls_assert ();
+              goto done;
+            }
+        }
+
+      elements_in_bag = gnutls_pkcs12_bag_get_count (bag);
+      if (elements_in_bag < 0)
+        {
+          gnutls_assert ();
+          goto done;
+        }
+
+      for (i = 0; i < elements_in_bag; i++)
+        {
+          int type;
+          gnutls_datum_t data;
+
+          type = gnutls_pkcs12_bag_get_type (bag, i);
+          if (type < 0)
+            {
+              gnutls_assert ();
+              goto done;
+            }
+
+          ret = gnutls_pkcs12_bag_get_data (bag, i, &data);
+          if (ret < 0)
+            {
+              gnutls_assert ();
+              goto done;
+            }
+
+          switch (type)
+            {
+            case GNUTLS_BAG_PKCS8_ENCRYPTED_KEY:
+            case GNUTLS_BAG_PKCS8_KEY:
+              if (*key != NULL) /* too simple to continue */
+                {
+                  gnutls_assert ();
+                  break;
+                }
+
+              ret = gnutls_x509_privkey_init (key);
+              if (ret < 0)
+                {
+                  gnutls_assert ();
+                  goto done;
+                }
+
+              ret = gnutls_x509_privkey_import_pkcs8
+                (*key, &data, GNUTLS_X509_FMT_DER, password,
+                 type == GNUTLS_BAG_PKCS8_KEY ? GNUTLS_PKCS_PLAIN : 0);
+              if (ret < 0)
+                {
+                  gnutls_assert ();
+                  gnutls_x509_privkey_deinit (*key);
+                  goto done;
+                }
+
+              key_id_size = sizeof (key_id);
+              ret =
+                gnutls_x509_privkey_get_key_id (*key, 0, key_id,
+                                                &key_id_size);
+              if (ret < 0)
+                {
+                  gnutls_assert ();
+                  gnutls_x509_privkey_deinit (*key);
+                  goto done;
+                }
+
+              privkey_ok = 1;   /* break */
+              break;
+            default:
+              break;
+            }
+        }
+
+      idx++;
+      gnutls_pkcs12_bag_deinit (bag);
+
+      if (privkey_ok != 0)      /* private key was found */
+        break;
+    }
+
+  if (privkey_ok == 0)          /* no private key */
+    {
+      gnutls_assert ();
+      return GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE;
+    }
+
+  /* now find the corresponding certificate 
+   */
+  idx = 0;
+  bag = NULL;
+  for (;;)
+    {
+      int elements_in_bag;
+      int i;
+
+      ret = gnutls_pkcs12_bag_init (&bag);
+      if (ret < 0)
+        {
+          bag = NULL;
+          gnutls_assert ();
+          goto done;
+        }
+
+      ret = gnutls_pkcs12_get_bag (p12, idx, bag);
+      if (ret == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE)
+        break;
+      if (ret < 0)
+        {
+          gnutls_assert ();
+          goto done;
+        }
+
+      ret = gnutls_pkcs12_bag_get_type (bag, 0);
+      if (ret < 0)
+        {
+          gnutls_assert ();
+          goto done;
+        }
+
+      if (ret == GNUTLS_BAG_ENCRYPTED)
+        {
+          ret = gnutls_pkcs12_bag_decrypt (bag, password);
+          if (ret < 0)
+            {
+              gnutls_assert ();
+              goto done;
+            }
+        }
+
+      elements_in_bag = gnutls_pkcs12_bag_get_count (bag);
+      if (elements_in_bag < 0)
+        {
+          gnutls_assert ();
+          goto done;
+        }
+
+      for (i = 0; i < elements_in_bag; i++)
+        {
+          int type;
+          gnutls_datum_t data;
+
+          type = gnutls_pkcs12_bag_get_type (bag, i);
+          if (type < 0)
+            {
+              gnutls_assert ();
+              goto done;
+            }
+
+          ret = gnutls_pkcs12_bag_get_data (bag, i, &data);
+          if (ret < 0)
+            {
+              gnutls_assert ();
+              goto done;
+            }
+
+          switch (type)
+            {
+            case GNUTLS_BAG_CERTIFICATE:
+              if (*cert != NULL)        /* no need to set it again */
+                {
+                  gnutls_assert ();
+                  break;
+                }
+
+              ret = gnutls_x509_crt_init (cert);
+              if (ret < 0)
+                {
+                  gnutls_assert ();
+                  goto done;
+                }
+
+              ret =
+                gnutls_x509_crt_import (*cert, &data, GNUTLS_X509_FMT_DER);
+              if (ret < 0)
+                {
+                  gnutls_assert ();
+                  gnutls_x509_crt_deinit (*cert);
+                  goto done;
+                }
+
+              /* check if the key id match */
+              cert_id_size = sizeof (cert_id);
+              ret =
+                gnutls_x509_crt_get_key_id (*cert, 0, cert_id, &cert_id_size);
+              if (ret < 0)
+                {
+                  gnutls_assert ();
+                  gnutls_x509_crt_deinit (*cert);
+                  goto done;
+                }
+
+              if (memcmp (cert_id, key_id, cert_id_size) != 0)
+                {               /* they don't match - skip the certificate */
+                  gnutls_x509_crt_deinit (*cert);
+                  *cert = NULL;
+                }
+              break;
+
+            case GNUTLS_BAG_CRL:
+              if (*crl != NULL)
+                {
+                  gnutls_assert ();
+                  break;
+                }
+
+              ret = gnutls_x509_crl_init (crl);
+              if (ret < 0)
+                {
+                  gnutls_assert ();
+                  goto done;
+                }
+
+              ret = gnutls_x509_crl_import (*crl, &data, GNUTLS_X509_FMT_DER);
+              if (ret < 0)
+                {
+                  gnutls_assert ();
+                  gnutls_x509_crl_deinit (*crl);
+                  goto done;
+                }
+              break;
+
+            case GNUTLS_BAG_ENCRYPTED:
+              /* XXX Bother to recurse one level down?  Unlikely to
+                 use the same password anyway. */
+            case GNUTLS_BAG_EMPTY:
+            default:
+              break;
+            }
+        }
+
+      idx++;
+      gnutls_pkcs12_bag_deinit (bag);
+    }
+
+  ret = 0;
+
+done:
+  if (bag)
+    gnutls_pkcs12_bag_deinit (bag);
+
+  return ret;
+}