/*
* This file is part of Cockpit.
*
* Copyright (C) 2013 Red Hat, Inc.
*
* Cockpit 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.
*
* Cockpit 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 Cockpit; If not, see .
*/
#include "config.h"
#include "cockpitcertificate.h"
#include "common/cockpitconf.h"
#include "common/cockpitlog.h"
#include "common/cockpitmemory.h"
#include
#include
#include
#include
static gchar *
get_common_name (void)
{
int ret;
gchar *hostname = NULL;
gchar *cn = NULL;
hostname = g_malloc (HOST_NAME_MAX + 1);
if (!hostname)
return NULL;
ret = gethostname (hostname, HOST_NAME_MAX);
if (ret < 0 || g_str_equal (hostname, ""))
cn = g_strdup ("localhost");
else
cn = g_strdup (hostname);
g_free (hostname);
return cn;
}
static gchar *
get_machine_id (void)
{
static const char HEX[] = "0123456789abcdef";
gchar *content;
gchar *machine_id = NULL;
if (g_file_get_contents ("/etc/machine-id", &content, NULL, NULL))
machine_id = g_strstrip (g_strcanon (content, HEX, ' '));
return machine_id;
}
static gchar *
generate_subject (void)
{
gchar *cn;
gchar *machine_id;
gchar *subject;
/*
* HACK: We have to use a unique value in DN because otherwise
* firefox hangs.
*
* https://bugzilla.redhat.com/show_bug.cgi?id=1204670
*
* In addition we have to generate the certificate with CA:TRUE
* because old versions of NSS refuse to process self-signed
* certificates if that's not the case.
*
*/
cn = get_common_name ();
machine_id = get_machine_id ();
if (machine_id)
{
subject = g_strdup_printf ("/O=%s/CN=%s",
machine_id, cn);
}
else
{
subject = g_strdup_printf ("/CN=%s", cn);
}
g_free (cn);
g_free (machine_id);
return subject;
}
static gboolean
openssl_make_dummy_cert (const gchar *key_file,
const gchar *out_file,
GError **error)
{
gboolean ret = FALSE;
gint exit_status;
gchar *stderr_str = NULL;
gchar *command_line = NULL;
gchar *subject = generate_subject ();
const gchar *argv[] = {
"openssl",
"req", "-x509",
"-days", "36500",
"-newkey", "rsa:2048",
"-keyout", key_file,
"-keyform", "PEM",
"-nodes",
"-out", out_file,
"-outform", "PEM",
"-subj", subject,
NULL
};
command_line = g_strjoinv (" ", (gchar **)argv);
g_info ("Generating temporary certificate using: %s", command_line);
if (!g_spawn_sync (NULL, (gchar **)argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL,
NULL, &stderr_str, &exit_status, error) ||
!g_spawn_check_exit_status (exit_status, error))
{
g_warning ("%s", stderr_str);
g_prefix_error (error, "Error generating temporary self-signed dummy cert using openssl: ");
goto out;
}
ret = TRUE;
out:
g_free (stderr_str);
g_free (command_line);
g_free (subject);
return ret;
}
static gboolean
sscg_make_dummy_cert (const gchar *key_file,
const gchar *cert_file,
const gchar *ca_file,
GError **error)
{
gboolean ret = FALSE;
gint exit_status;
gchar *stderr_str = NULL;
gchar *command_line = NULL;
gchar *cn = get_common_name ();
gchar *machine_id = get_machine_id ();
gchar *org;
if (machine_id)
org = machine_id;
else
org = "";
const gchar *argv[] = {
"sscg", "--quiet",
"--lifetime", "3650",
"--key-strength", "2048",
"--cert-key-file", key_file,
"--cert-file", cert_file,
"--ca-file", ca_file,
"--hostname", cn,
"--organization", org,
NULL
};
command_line = g_strjoinv (" ", (gchar **)argv);
g_info ("Generating temporary certificate using: %s", command_line);
if (!g_spawn_sync (NULL, (gchar **)argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL,
NULL, &stderr_str, &exit_status, error) ||
!g_spawn_check_exit_status (exit_status, error))
{
/* Failure of SSCG is non-fatal */
g_info ("Error generating temporary dummy cert using sscg, "
"falling back to openssl");
g_clear_error (error);
goto out;
}
ret = TRUE;
out:
g_free (stderr_str);
g_free (command_line);
g_free (machine_id);
g_free (cn);
return ret;
}
static gchar *
create_temp_file (const gchar *directory,
const gchar *templ,
GError **error)
{
gchar *path;
gint fd;
path = g_build_filename (directory, templ, NULL);
fd = g_mkstemp (path);
if (fd < 0)
{
g_set_error (error, G_FILE_ERROR,
g_file_error_from_errno (errno),
"Couldn't create temporary file: %s: %m", path);
g_free (path);
return NULL;
}
close (fd);
return path;
}
static gchar *
generate_temp_cert (const gchar *dir,
GError **error)
{
gchar *cert_path = NULL;
gchar *ca_path = NULL;
gchar *tmp_key = NULL;
gchar *tmp_pem = NULL;
gchar *cert_data = NULL;
gchar *pem_data = NULL;
gchar *key_data = NULL;
gchar *ret = NULL;
cert_path = g_build_filename (dir, "0-self-signed.cert", NULL);
/* Create the CA cert with a .pem suffix so it's not automatically loaded */
ca_path = g_build_filename (dir, "0-self-signed-ca.pem", NULL);
/* Generate self-signed cert, if it does not exist */
if (g_file_test (cert_path, G_FILE_TEST_EXISTS))
{
ret = cert_path;
cert_path = NULL;
goto out;
}
if (g_mkdir_with_parents (dir, 0700) != 0)
{
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"Error creating directory `%s': %m",
dir);
goto out;
}
/* First, try to create a private CA and certificate using SSCG */
if (sscg_make_dummy_cert (cert_path, cert_path, ca_path, error))
{
/* Creation with SSCG succeeded, so we are done now */
ret = cert_path;
cert_path = NULL;
goto out;
}
error = NULL;
/* Fall back to using the openssl CLI */
tmp_key = create_temp_file (dir, "0-self-signed.XXXXXX.tmp", error);
if (!tmp_key)
goto out;
tmp_pem = create_temp_file (dir, "0-self-signed.XXXXXX.tmp", error);
if (!tmp_pem)
goto out;
if (!openssl_make_dummy_cert (tmp_key, tmp_pem, error))
goto out;
if (!g_file_get_contents (tmp_key, &key_data, NULL, error))
goto out;
if (!g_file_get_contents (tmp_pem, &pem_data, NULL, error))
goto out;
cert_data = g_strdup_printf ("%s\n%s\n", pem_data, key_data);
if (!g_file_set_contents (cert_path, cert_data, -1, error))
goto out;
ret = cert_path;
cert_path = NULL;
out:
g_free (cert_path);
g_free (ca_path);
cockpit_memory_clear (key_data, -1);
g_free (key_data);
g_free (pem_data);
cockpit_memory_clear (cert_data, -1);
g_free (cert_data);
if (tmp_key)
g_unlink (tmp_key);
if (tmp_pem)
g_unlink (tmp_pem);
g_free (tmp_key);
g_free (tmp_pem);
return ret;
}
static gint
ptr_strcmp (const gchar **a,
const gchar **b)
{
return g_strcmp0 (*a, *b);
}
static gchar *
load_cert_from_dir (const gchar *dir_name,
GError **error)
{
gchar *ret = NULL;
GDir *dir;
const gchar *name;
GPtrArray *p;
p = g_ptr_array_new ();
dir = g_dir_open (dir_name, 0, error);
if (dir == NULL)
goto out;
while ((name = g_dir_read_name (dir)) != NULL)
{
if (!g_str_has_suffix (name, ".cert"))
continue;
g_ptr_array_add (p, g_strdup_printf ("%s/%s", dir_name, name));
}
g_ptr_array_sort (p, (GCompareFunc)ptr_strcmp);
if (p->len > 0)
{
ret = p->pdata[p->len - 1];
p->pdata[p->len - 1] = NULL;
}
out:
if (dir != NULL)
g_dir_close (dir);
g_ptr_array_foreach (p, (GFunc)g_free, NULL);
g_ptr_array_free (p, TRUE);
return ret;
}
gchar *
cockpit_certificate_locate (gboolean create_if_necessary,
GError **error)
{
const gchar * const* dirs = cockpit_conf_get_dirs ();
GError *local_error = NULL;
gchar *cert_dir;
gchar *cert_path;
gint i;
for (i = 0; dirs[i]; i++)
{
cert_dir = g_build_filename (dirs[i], "cockpit", "ws-certs.d", NULL);
cert_path = load_cert_from_dir (cert_dir, &local_error);
if (local_error != NULL)
{
g_propagate_prefixed_error (error, local_error,
"Error loading certificates from %s: ",
cert_dir);
g_free (cert_dir);
return NULL;
}
g_free (cert_dir);
if (cert_path)
return cert_path;
}
cert_dir = g_build_filename (dirs[0], "cockpit", "ws-certs.d", NULL);
if (create_if_necessary)
{
cert_path = generate_temp_cert (cert_dir, error);
}
else
{
cert_path = NULL;
g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
"No certificate found in dir: %s", cert_dir);
}
g_free (cert_dir);
return cert_path;
}
/*
* When running on GLib earlier than 2.44 we have to do our own
* certificate chain loading. This can be removed once we only
* support GLib 2.44 and later.
*
* GIO - GLib Input, Output and Certificateing Library
*
* Copyright (C) 2010 Red Hat, Inc.
*
* This library 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 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, see .
*/
#define PEM_CERTIFICATE_HEADER "-----BEGIN CERTIFICATE-----"
#define PEM_CERTIFICATE_FOOTER "-----END CERTIFICATE-----"
#define PEM_PKCS1_PRIVKEY_HEADER "-----BEGIN RSA PRIVATE KEY-----"
#define PEM_PKCS1_PRIVKEY_FOOTER "-----END RSA PRIVATE KEY-----"
#define PEM_PKCS8_PRIVKEY_HEADER "-----BEGIN PRIVATE KEY-----"
#define PEM_PKCS8_PRIVKEY_FOOTER "-----END PRIVATE KEY-----"
#define PEM_PKCS8_ENCRYPTED_HEADER "-----BEGIN ENCRYPTED PRIVATE KEY-----"
#define PEM_PKCS8_ENCRYPTED_FOOTER "-----END ENCRYPTED PRIVATE KEY-----"
static gchar *
parse_private_key (const gchar *data,
gsize data_len,
gboolean required,
GError **error)
{
const gchar *start, *end, *footer;
start = g_strstr_len (data, data_len, PEM_PKCS1_PRIVKEY_HEADER);
if (start)
footer = PEM_PKCS1_PRIVKEY_FOOTER;
else
{
start = g_strstr_len (data, data_len, PEM_PKCS8_PRIVKEY_HEADER);
if (start)
footer = PEM_PKCS8_PRIVKEY_FOOTER;
else
{
start = g_strstr_len (data, data_len, PEM_PKCS8_ENCRYPTED_HEADER);
if (start)
{
g_set_error_literal (error, G_TLS_ERROR, G_TLS_ERROR_BAD_CERTIFICATE,
_("Cannot decrypt PEM-encoded private key"));
}
else if (required)
{
g_set_error_literal (error, G_TLS_ERROR, G_TLS_ERROR_BAD_CERTIFICATE,
_("No PEM-encoded private key found"));
}
return NULL;
}
}
end = g_strstr_len (start, data_len - (data - start), footer);
if (!end)
{
g_set_error_literal (error, G_TLS_ERROR, G_TLS_ERROR_BAD_CERTIFICATE,
_("Could not parse PEM-encoded private key"));
return NULL;
}
end += strlen (footer);
while (*end == '\r' || *end == '\n')
end++;
return g_strndup (start, end - start);
}
static gchar *
parse_next_pem_certificate (const gchar **data,
const gchar *data_end,
gboolean required,
GError **error)
{
const gchar *start, *end;
start = g_strstr_len (*data, data_end - *data, PEM_CERTIFICATE_HEADER);
if (!start)
{
if (required)
{
g_set_error_literal (error, G_TLS_ERROR, G_TLS_ERROR_BAD_CERTIFICATE,
_("No PEM-encoded certificate found"));
}
return NULL;
}
end = g_strstr_len (start, data_end - start, PEM_CERTIFICATE_FOOTER);
if (!end)
{
g_set_error_literal (error, G_TLS_ERROR, G_TLS_ERROR_BAD_CERTIFICATE,
_("Could not parse PEM-encoded certificate"));
return NULL;
}
end += strlen (PEM_CERTIFICATE_FOOTER);
while (*end == '\r' || *end == '\n')
end++;
*data = end;
return g_strndup (start, end - start);
}
static GSList *
parse_and_create_certificate_list (const gchar *data,
gsize data_len,
GError **error)
{
GSList *first_pem_list = NULL, *pem_list = NULL;
gchar *first_pem;
const gchar *p, *end;
p = data;
end = p + data_len;
/* Make sure we can load, at least, one certificate. */
first_pem = parse_next_pem_certificate (&p, end, TRUE, error);
if (!first_pem)
return NULL;
/* Create a list with a single element. If we load more certificates
* below, we will concatenate the two lists at the end. */
first_pem_list = g_slist_prepend (first_pem_list, first_pem);
/* If we read one certificate successfully, let's see if we can read
* some more. If not, we will simply return a list with the first one.
*/
while (p && *p)
{
gchar *cert_pem;
GError *error = NULL;
cert_pem = parse_next_pem_certificate (&p, end, FALSE, &error);
if (error)
{
g_slist_free_full (pem_list, g_free);
g_error_free (error);
return first_pem_list;
}
else if (!cert_pem)
{
break;
}
pem_list = g_slist_prepend (pem_list, cert_pem);
}
pem_list = g_slist_concat (pem_list, first_pem_list);
return pem_list;
}
static GTlsCertificate *
tls_certificate_new_internal (const gchar *certificate_pem,
const gchar *private_key_pem,
GTlsCertificate *issuer,
GError **error)
{
GObject *cert;
GTlsBackend *backend;
backend = g_tls_backend_get_default ();
cert = g_initable_new (g_tls_backend_get_certificate_type (backend),
NULL, error,
"certificate-pem", certificate_pem,
"private-key-pem", private_key_pem,
"issuer", issuer,
NULL);
return G_TLS_CERTIFICATE (cert);
}
static GTlsCertificate *
create_certificate_chain_from_list (GSList *pem_list,
const gchar *key_pem)
{
GTlsCertificate *cert = NULL, *issuer = NULL, *root = NULL;
GTlsCertificateFlags flags;
GSList *pem;
pem = pem_list;
while (pem)
{
const gchar *key = NULL;
/* Private key belongs only to the first certificate. */
if (!pem->next)
key = key_pem;
/* We assume that the whole file is a certificate chain, so we use
* each certificate as the issuer of the next one (list is in
* reverse order).
*/
issuer = cert;
cert = tls_certificate_new_internal (pem->data, key, issuer, NULL);
if (issuer)
g_object_unref (issuer);
if (!cert)
return NULL;
/* root will point to the last certificate in the file. */
if (!root)
root = cert;
pem = g_slist_next (pem);
}
/* Verify that the certificates form a chain. (We don't care at this
* point if there are other problems with it.)
*/
flags = g_tls_certificate_verify (cert, NULL, root);
if (flags & G_TLS_CERTIFICATE_UNKNOWN_CA)
{
/* It wasn't a chain, it's just a bunch of unrelated certs. */
g_clear_object (&cert);
}
return cert;
}
static GTlsCertificate *
parse_and_create_certificate (const gchar *data,
gsize data_len,
const gchar *key_pem,
GError **error)
{
GSList *pem_list;
GTlsCertificate *cert;
pem_list = parse_and_create_certificate_list (data, data_len, error);
if (!pem_list)
return NULL;
/* We don't pass the error here because, if it fails, we still want to
* load and return the first certificate.
*/
cert = create_certificate_chain_from_list (pem_list, key_pem);
if (!cert)
{
GSList *last = NULL;
/* Get the first certificate (which is the last one as the list is
* in reverse order).
*/
last = g_slist_last (pem_list);
cert = tls_certificate_new_internal (last->data, key_pem, NULL, error);
}
g_slist_free_full (pem_list, g_free);
return cert;
}
static GTlsCertificate *
tls_certificate_new_from_pem (const gchar *data,
gssize length,
GError **error)
{
GError *child_error = NULL;
gchar *key_pem;
GTlsCertificate *cert;
g_return_val_if_fail (data != NULL, NULL);
g_return_val_if_fail (error == NULL || *error == NULL, NULL);
if (length == -1)
length = strlen (data);
key_pem = parse_private_key (data, length, TRUE, &child_error);
if (child_error != NULL)
{
g_propagate_error (error, child_error);
return NULL;
}
cert = parse_and_create_certificate (data, length, key_pem, error);
g_free (key_pem);
return cert;
}
static GTlsCertificate *
tls_certificate_new_from_file (const gchar *file,
GError **error)
{
GTlsCertificate *cert;
gchar *contents;
gsize length;
if (!g_file_get_contents (file, &contents, &length, error))
return NULL;
cert = tls_certificate_new_from_pem (contents, length, error);
g_free (contents);
return cert;
}
static gint
tls_certificate_count (GTlsCertificate *cert)
{
gint count = 0;
while (cert != NULL)
{
cert = g_tls_certificate_get_issuer (cert);
count++;
}
return count;
}
GTlsCertificate *
cockpit_certificate_load (const gchar *cert_path,
GError **error)
{
GTlsCertificate *cert;
cert = tls_certificate_new_from_file (cert_path, error);
if (cert == NULL)
g_prefix_error (error, "%s: ", cert_path);
else
g_debug ("loaded %d certificates from %s", tls_certificate_count (cert), cert_path);
return cert;
}