GnuTLS: Add supporting certificates from PKCS#12 file
authorDavid Woodhouse <David.Woodhouse@intel.com>
Thu, 31 May 2012 18:54:50 +0000 (19:54 +0100)
committerDavid Woodhouse <David.Woodhouse@intel.com>
Thu, 31 May 2012 20:40:30 +0000 (21:40 +0100)
Signed-off-by: David Woodhouse <David.Woodhouse@intel.com>
gnutls.c

index 4df3a90..d80f6b6 100644 (file)
--- a/gnutls.c
+++ b/gnutls.c
@@ -397,13 +397,43 @@ static int load_pkcs12_certificate(struct openconnect_info *vpninfo,
        return 0;
 }
 
+/* Older versions of GnuTLS didn't actually bother to check this, so we'll
+   do it for them. */
+static int check_issuer_sanity(gnutls_x509_crt_t cert, gnutls_x509_crt_t issuer)
+{
+#if GNUTLS_VERSION_NUMBER > 0x300014
+       return 0;
+#else
+       unsigned char id1[512], id2[512];
+       size_t id1_size = 512, id2_size = 512;
+       int err;
+
+       err = gnutls_x509_crt_get_authority_key_id(cert, id1, &id1_size, NULL);
+       if (err)
+               return 0;
+
+       err = gnutls_x509_crt_get_subject_key_id(issuer, id2, &id2_size, NULL);
+       if (err)
+               return 0;
+       if (id1_size == id2_size && !memcmp(id1, id2, id1_size))
+               return 0;
+
+       /* EEP! */
+       return -EIO;
+#endif
+}
+
 static int load_certificate(struct openconnect_info *vpninfo)
 {
        gnutls_datum_t fdata;
        gnutls_x509_privkey_t key = NULL;
-       gnutls_x509_crt_t cert = NULL;
        gnutls_x509_crl_t crl = NULL;
-       int err;
+       gnutls_x509_crt_t last_cert, cert = NULL;
+       gnutls_x509_crt_t *extra_certs = NULL, *supporting_certs = NULL;
+       unsigned int nr_supporting_certs, nr_extra_certs = 0;
+       int err; /* GnuTLS error */
+       int ret = 0; /* our error (zero or -errno) */
+       int i;
 
        if (vpninfo->cert_type == CERT_TYPE_TPM) {
                vpn_progress(vpninfo, PRG_ERR,
@@ -416,7 +446,7 @@ static int load_certificate(struct openconnect_info *vpninfo)
                             _("Using PKCS#11 certificate %s\n"), vpninfo->cert);
 
                err = gnutls_certificate_set_x509_key_file(vpninfo->https_cred,
-                                                          vpninfo->cert, 
+                                                          vpninfo->cert,
                                                           vpninfo->sslkey,
                                                           GNUTLS_X509_FMT_PEM);
                if (err) {
@@ -427,24 +457,24 @@ static int load_certificate(struct openconnect_info *vpninfo)
                }
                return 0;
        }
-               
+
        vpn_progress(vpninfo, PRG_TRACE,
                     _("Using certificate file %s\n"), vpninfo->cert);
-       
-       err = load_datum(vpninfo, &fdata, vpninfo->cert);
-       if (err)
-               return err;
+
+       ret = load_datum(vpninfo, &fdata, vpninfo->cert);
+       if (ret)
+               return ret;
 
        if (vpninfo->cert_type == CERT_TYPE_PKCS12 ||
            vpninfo->cert_type == CERT_TYPE_UNKNOWN) {
-               err = load_pkcs12_certificate(vpninfo, &fdata, &key, &cert,
-                                             NULL, NULL, &crl);
-               if (!err)
-                       goto got_cert;
-               else if (err <= 0) {
+               ret = load_pkcs12_certificate(vpninfo, &fdata, &key, &cert,
+                                             &extra_certs, &nr_extra_certs, &crl);
+               if (ret < 0) {
                        gnutls_free(fdata.data);
-                       return err;
-               }
+                       return ret;
+               } else if (!ret)
+                       goto got_cert;
+
                /* It returned NOT_PKCS12.
                   Fall through to try PEM formats. */
        }
@@ -465,13 +495,11 @@ static int load_certificate(struct openconnect_info *vpninfo)
                vpn_progress(vpninfo, PRG_TRACE,
                             _("Using private key file %s\n"), vpninfo->cert);
 
-               err = load_datum(vpninfo, &fdata, vpninfo->sslkey);
-               if (err) {
-                       gnutls_x509_crt_deinit(cert);
-                       return err;
-               }
+               ret = load_datum(vpninfo, &fdata, vpninfo->sslkey);
+               if (ret)
+                       goto out;
        }
-       
+
        gnutls_x509_privkey_init(&key);
        /* Try PKCS#1 (and PKCS#8 without password) first. GnuTLS doesn't
           support OpenSSL's old PKCS#1-based encrypted format. We should
@@ -495,9 +523,8 @@ static int load_certificate(struct openconnect_info *vpninfo)
                                vpn_progress(vpninfo, PRG_ERR,
                                             _("Failed to load private key as PKCS#8: %s\n"),
                                             gnutls_strerror(err));
-                               gnutls_x509_crt_deinit(cert);
-                               gnutls_free(fdata.data);
-                               return -EINVAL;
+                               ret = -EINVAL;
+                               goto out;
                        }
                        if (pass) {
                                vpn_progress(vpninfo, PRG_ERR,
@@ -507,41 +534,122 @@ static int load_certificate(struct openconnect_info *vpninfo)
                        err = request_passphrase(vpninfo, &pass,
                                                 _("Enter PEM pass phrase:"));
                        if (err) {
-                               gnutls_x509_crt_deinit(cert);
-                               gnutls_free(fdata.data);
-                               return -EINVAL;
+                               ret = -EINVAL;
+                               goto out;
                        }
                }
        }
  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;
+                       goto out;
                }
        }
-       /* 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. */
+
+       /* OpenSSL has problems with certificate chains — if there are
+          multiple certs with the same name, it doesn't necessarily
+          choose the _right_ one. (RT#1942)
+          Pick the right ones for ourselves and add them manually. */
+       last_cert = cert;
+       nr_supporting_certs = 1; /* Our starting cert */
+       while (1) {
+               gnutls_x509_crt_t issuer;
+               char name[80];
+               size_t namelen;
+
+               for (i = 0; i < nr_extra_certs; i++) {
+                       if (gnutls_x509_crt_check_issuer(last_cert, extra_certs[i]) &&
+                           !check_issuer_sanity(last_cert, extra_certs[i]))
+                               break;
+               }
+
+               if (i < nr_extra_certs) {
+                       issuer = extra_certs[i];
+               } else {
+                       err = gnutls_certificate_get_issuer(vpninfo->https_cred,
+                                                           last_cert, &issuer, 0);
+                       if (err) {
+                               printf("can't get issuer for %p: %s\n",
+                                      last_cert, gnutls_strerror(err));
+                               break;
+                       }
+               }
+
+               /* The check_issuer_sanity() function works fine as a workaround where
+                  it was used above, but when gnutls_certificate_get_issuer() returns
+                  a bogus cert, there's nothing we can do to fix it up. We don't get
+                  to iterate over all the available certs like we can over our own
+                  list. */
+               if (check_issuer_sanity(last_cert, issuer)) {
+                       /* Hm, is there a bug reference for this? Or just the git commit
+                          reference (c1ef7efb in master, 5196786c in gnutls_3_0_x-2)? */
+                       vpn_progress(vpninfo, PRG_ERR,
+                                    _("WARNING: GnuTLS returned incorrect issuer certs; authentication may fail!\n"));
+                       break;
+               }
+
+               if (issuer == last_cert)
+                       break;
+
+               /* OK, we found a new cert to add to our chain. */
+               supporting_certs = realloc(supporting_certs,
+                                          sizeof(cert) * ++nr_supporting_certs);
+               if (!supporting_certs) {
+                       vpn_progress(vpninfo, PRG_ERR,
+                                    _("Failed to allocate memory for supporting certificates\n"));
+                       /* The world is probably about to end, but try without them anyway */
+                       break;
+               }
+
+               /* First time we actually allocated an array? Copy the first cert into it */
+               if (nr_supporting_certs == 2)
+                       supporting_certs[0] = cert;
+
+               /* Append the new one */
+               supporting_certs[nr_supporting_certs-1] = issuer;
+               last_cert = issuer;
+
+               /* Logging. */
+               sprintf(name, "<unknown>");
+               namelen = sizeof(name);
+               if (gnutls_x509_crt_get_dn_by_oid(issuer, GNUTLS_OID_X520_COMMON_NAME, 0, 0,
+                                                 name, &namelen) &&
+                   gnutls_x509_crt_get_dn(issuer, name, &namelen))
+                       sprintf(name, "<unknown>");
+
+               vpn_progress(vpninfo, PRG_DEBUG,
+                            _("Adding supporting CA '%s'\n"), name);
+       }
+
        err = gnutls_certificate_set_x509_key(vpninfo->https_cred,
-                                             &cert, 1, key);
-       gnutls_x509_privkey_deinit(key);
-       gnutls_x509_crt_deinit(cert);
+                                             supporting_certs ? supporting_certs : &cert,
+                                             supporting_certs ? 1 : nr_supporting_certs,
+                                             key);
        if (err) {
                vpn_progress(vpninfo, PRG_ERR,
                             _("Setting certificate failed: %s\n"),
                             gnutls_strerror(err));
-               return -EIO;
+               ret = -EIO;
        }
-       return 0;
+ out:
+       if (crl)
+               gnutls_x509_crl_deinit(crl);
+       if (key)
+               gnutls_x509_privkey_deinit(key);
+       if (cert)
+               gnutls_x509_crt_deinit(cert);
+       for (i = 0; i < nr_extra_certs; i++)
+               gnutls_x509_crt_deinit(extra_certs[i]);
+       free(extra_certs);
+       free(supporting_certs);
+       gnutls_free(fdata.data);
+       return ret;
 }
 
 static int get_cert_fingerprint(struct openconnect_info *vpninfo,