From b6267745ede04b3c943bc02e004bdb9347e0f564 Mon Sep 17 00:00:00 2001 From: Andrea Spadaccini <spadaccio@google.com> Date: Tue, 6 Sep 2011 18:14:51 +0100 Subject: [PATCH] Implementation of TLS-protected SPICE connections Added support for TLS-protected SPICE connections: client/gnt_cluster.py, cli.py: * added three new parameters to renew-crypto (--new-spice-certificate, --spice-certificate, --spice-ca-certificate) and their validation. utils/x509.py: * changed GenerateSelfSignedSslCert so that now also returns the generated key and certificate; * added missing return value in the docstring of GenerateSelfSignedX509Cert. lib/bootstrap.py: * changed the signatures of the relevant functions and implemented certificates generation/writing. tools/cfupgrade: * changed GenerateClusterCrypto invocation to reflect the new signature; * added SPICE certificate names. lib/errors.py: * added the X509CertError class. lib/hypervisor/hv_kvm.py: * silenced pylint warning R0915 Signed-off-by: Andrea Spadaccini <spadaccio@google.com> Reviewed-by: Michael Hanselmann <hansmi@google.com> --- lib/bootstrap.py | 46 +++++++++++++++-- lib/cli.py | 18 +++++++ lib/client/gnt_cluster.py | 102 +++++++++++++++++++++++++++++--------- lib/errors.py | 8 +++ lib/hypervisor/hv_kvm.py | 2 +- lib/utils/x509.py | 5 ++ tools/cfgupgrade | 14 ++++-- 7 files changed, 162 insertions(+), 33 deletions(-) diff --git a/lib/bootstrap.py b/lib/bootstrap.py index a058c568e..85c2251ba 100644 --- a/lib/bootstrap.py +++ b/lib/bootstrap.py @@ -88,10 +88,14 @@ def GenerateHmacKey(file_name): backup=True) -def GenerateClusterCrypto(new_cluster_cert, new_rapi_cert, new_confd_hmac_key, - new_cds, rapi_cert_pem=None, cds=None, +def GenerateClusterCrypto(new_cluster_cert, new_rapi_cert, new_spice_cert, + new_confd_hmac_key, new_cds, + rapi_cert_pem=None, spice_cert_pem=None, + spice_cacert_pem=None, cds=None, nodecert_file=constants.NODED_CERT_FILE, rapicert_file=constants.RAPI_CERT_FILE, + spicecert_file=constants.SPICE_CERT_FILE, + spicecacert_file=constants.SPICE_CACERT_FILE, hmackey_file=constants.CONFD_HMAC_KEY, cds_file=constants.CLUSTER_DOMAIN_SECRET_FILE): """Updates the cluster certificates, keys and secrets. @@ -100,18 +104,29 @@ def GenerateClusterCrypto(new_cluster_cert, new_rapi_cert, new_confd_hmac_key, @param new_cluster_cert: Whether to generate a new cluster certificate @type new_rapi_cert: bool @param new_rapi_cert: Whether to generate a new RAPI certificate + @type new_spice_cert: bool + @param new_spice_cert: Whether to generate a new SPICE certificate @type new_confd_hmac_key: bool @param new_confd_hmac_key: Whether to generate a new HMAC key @type new_cds: bool @param new_cds: Whether to generate a new cluster domain secret @type rapi_cert_pem: string @param rapi_cert_pem: New RAPI certificate in PEM format + @type spice_cert_pem: string + @param spice_cert_pem: New SPICE certificate in PEM format + @type spice_cacert_pem: string + @param spice_cacert_pem: Certificate of the CA that signed the SPICE + certificate, in PEM format @type cds: string @param cds: New cluster domain secret @type nodecert_file: string @param nodecert_file: optional override of the node cert file path @type rapicert_file: string @param rapicert_file: optional override of the rapi cert file path + @type spicecert_file: string + @param spicecert_file: optional override of the spice cert file path + @type spicecacert_file: string + @param spicecacert_file: optional override of the spice CA cert file path @type hmackey_file: string @param hmackey_file: optional override of the hmac key file path @@ -145,6 +160,31 @@ def GenerateClusterCrypto(new_cluster_cert, new_rapi_cert, new_confd_hmac_key, logging.debug("Generating new RAPI certificate at %s", rapicert_file) utils.GenerateSelfSignedSslCert(rapicert_file) + # SPICE + spice_cert_exists = os.path.exists(spicecert_file) + spice_cacert_exists = os.path.exists(spicecacert_file) + if spice_cert_pem: + # spice_cert_pem implies also spice_cacert_pem + logging.debug("Writing SPICE certificate at %s", spicecert_file) + utils.WriteFile(spicecert_file, data=spice_cert_pem, backup=True) + logging.debug("Writing SPICE CA certificate at %s", spicecacert_file) + utils.WriteFile(spicecacert_file, data=spice_cacert_pem, backup=True) + elif new_spice_cert or not spice_cert_exists: + if spice_cert_exists: + utils.CreateBackup(spicecert_file) + if spice_cacert_exists: + utils.CreateBackup(spicecacert_file) + + logging.debug("Generating new self-signed SPICE certificate at %s", + spicecert_file) + (_, cert_pem) = utils.GenerateSelfSignedSslCert(spicecert_file) + + # Self-signed certificate -> the public certificate is also the CA public + # certificate + logging.debug("Writing the public certificate to %s", + spicecert_file) + utils.io.WriteFile(spicecacert_file, mode=0400, data=cert_pem) + # Cluster domain secret if cds: logging.debug("Writing cluster domain secret to %s", cds_file) @@ -166,7 +206,7 @@ def _InitGanetiServerSetup(master_name): """ # Generate cluster secrets - GenerateClusterCrypto(True, False, False, False) + GenerateClusterCrypto(True, False, False, False, False) result = utils.RunCmd([constants.DAEMON_UTIL, "start", constants.NODED]) if result.failed: diff --git a/lib/cli.py b/lib/cli.py index d7e497e58..3f8d4979d 100644 --- a/lib/cli.py +++ b/lib/cli.py @@ -109,6 +109,7 @@ __all__ = [ "NEW_CONFD_HMAC_KEY_OPT", "NEW_RAPI_CERT_OPT", "NEW_SECONDARY_OPT", + "NEW_SPICE_CERT_OPT", "NIC_PARAMS_OPT", "NODE_FORCE_JOIN_OPT", "NODE_LIST_OPT", @@ -159,6 +160,8 @@ __all__ = [ "SHOWCMD_OPT", "SHUTDOWN_TIMEOUT_OPT", "SINGLE_NODE_OPT", + "SPICE_CACERT_OPT", + "SPICE_CERT_OPT", "SRC_DIR_OPT", "SRC_NODE_OPT", "SUBMIT_OPT", @@ -1083,6 +1086,21 @@ NEW_RAPI_CERT_OPT = cli_option("--new-rapi-certificate", dest="new_rapi_cert", help=("Generate a new self-signed RAPI" " certificate")) +SPICE_CERT_OPT = cli_option("--spice-certificate", dest="spice_cert", + default=None, + help="File containing new SPICE certificate") + +SPICE_CACERT_OPT = cli_option("--spice-ca-certificate", dest="spice_cacert", + default=None, + help="File containing the certificate of the CA" + " which signed the SPICE certificate") + +NEW_SPICE_CERT_OPT = cli_option("--new-spice-certificate", + dest="new_spice_cert", default=None, + action="store_true", + help=("Generate a new self-signed SPICE" + " certificate")) + NEW_CONFD_HMAC_KEY_OPT = cli_option("--new-confd-hmac-key", dest="new_confd_hmac_key", default=False, action="store_true", diff --git a/lib/client/gnt_cluster.py b/lib/client/gnt_cluster.py index b61e8e341..5637774f3 100644 --- a/lib/client/gnt_cluster.py +++ b/lib/client/gnt_cluster.py @@ -646,9 +646,45 @@ def SearchTags(opts, args): ToStdout("%s %s", path, tag) -def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename, - new_confd_hmac_key, new_cds, cds_filename, - force): +def _ReadAndVerifyCert(cert_filename, verify_private_key=False): + """Reads and verifies an X509 certificate. + + @type cert_filename: string + @param cert_filename: the path of the file containing the certificate to + verify encoded in PEM format + @type verify_private_key: bool + @param verify_private_key: whether to verify the private key in addition to + the public certificate + @rtype: string + @return: a string containing the PEM-encoded certificate. + + """ + try: + pem = utils.ReadFile(cert_filename) + except IOError, err: + raise errors.X509CertError(cert_filename, + "Unable to read certificate: %s" % str(err)) + + try: + OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, pem) + except Exception, err: + raise errors.X509CertError(cert_filename, + "Unable to load certificate: %s" % str(err)) + + if verify_private_key: + try: + OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, pem) + except Exception, err: + raise errors.X509CertError(cert_filename, + "Unable to load private key: %s" % str(err)) + + return pem + + +def _RenewCrypto(new_cluster_cert, new_rapi_cert, #pylint: disable=R0911 + rapi_cert_filename, new_spice_cert, spice_cert_filename, + spice_cacert_filename, new_confd_hmac_key, new_cds, + cds_filename, force): """Renews cluster certificates, keys and secrets. @type new_cluster_cert: bool @@ -657,6 +693,13 @@ def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename, @param new_rapi_cert: Whether to generate a new RAPI certificate @type rapi_cert_filename: string @param rapi_cert_filename: Path to file containing new RAPI certificate + @type new_spice_cert: bool + @param new_spice_cert: Whether to generate a new SPICE certificate + @type spice_cert_filename: string + @param spice_cert_filename: Path to file containing new SPICE certificate + @type spice_cacert_filename: string + @param spice_cacert_filename: Path to file containing the certificate of the + CA that signed the SPICE certificate @type new_confd_hmac_key: bool @param new_confd_hmac_key: Whether to generate a new HMAC key @type new_cds: bool @@ -678,27 +721,26 @@ def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename, " the same time.") return 1 - if rapi_cert_filename: - # Read and verify new certificate - try: - rapi_cert_pem = utils.ReadFile(rapi_cert_filename) - - OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, - rapi_cert_pem) - except Exception, err: # pylint: disable=W0703 - ToStderr("Can't load new RAPI certificate from %s: %s" % - (rapi_cert_filename, str(err))) - return 1 + if new_spice_cert and (spice_cert_filename or spice_cacert_filename): + ToStderr("When using --new-spice-certificate, the --spice-certificate" + " and --spice-ca-certificate must not be used.") + return 1 - try: - OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, rapi_cert_pem) - except Exception, err: # pylint: disable=W0703 - ToStderr("Can't load new RAPI private key from %s: %s" % - (rapi_cert_filename, str(err))) - return 1 + if bool(spice_cacert_filename) ^ bool(spice_cert_filename): + ToStderr("Both --spice-certificate and --spice-ca-certificate must be" + " specified.") + return 1 - else: - rapi_cert_pem = None + rapi_cert_pem, spice_cert_pem, spice_cacert_pem = (None, None, None) + try: + if rapi_cert_filename: + rapi_cert_pem = _ReadAndVerifyCert(rapi_cert_filename, True) + if spice_cert_filename: + spice_cert_pem = _ReadAndVerifyCert(spice_cert_filename, True) + spice_cacert_pem = _ReadAndVerifyCert(spice_cacert_filename) + except errors.X509CertError, err: + ToStderr("Unable to load X509 certificate from %s: %s", err[0], err[1]) + return 1 if cds_filename: try: @@ -718,10 +760,14 @@ def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename, def _RenewCryptoInner(ctx): ctx.feedback_fn("Updating certificates and keys") - bootstrap.GenerateClusterCrypto(new_cluster_cert, new_rapi_cert, + bootstrap.GenerateClusterCrypto(new_cluster_cert, + new_rapi_cert, + new_spice_cert, new_confd_hmac_key, new_cds, rapi_cert_pem=rapi_cert_pem, + spice_cert_pem=spice_cert_pem, + spice_cacert_pem=spice_cacert_pem, cds=cds) files_to_copy = [] @@ -732,6 +778,10 @@ def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename, if new_rapi_cert or rapi_cert_pem: files_to_copy.append(constants.RAPI_CERT_FILE) + if new_spice_cert or spice_cert_pem: + files_to_copy.append(constants.SPICE_CERT_FILE) + files_to_copy.append(constants.SPICE_CACERT_FILE) + if new_confd_hmac_key: files_to_copy.append(constants.CONFD_HMAC_KEY) @@ -760,6 +810,9 @@ def RenewCrypto(opts, args): return _RenewCrypto(opts.new_cluster_cert, opts.new_rapi_cert, opts.rapi_cert, + opts.new_spice_cert, + opts.spice_cert, + opts.spice_cacert, opts.new_confd_hmac_key, opts.new_cluster_domain_secret, opts.cluster_domain_secret, @@ -1348,7 +1401,8 @@ commands = { RenewCrypto, ARGS_NONE, [NEW_CLUSTER_CERT_OPT, NEW_RAPI_CERT_OPT, RAPI_CERT_OPT, NEW_CONFD_HMAC_KEY_OPT, FORCE_OPT, - NEW_CLUSTER_DOMAIN_SECRET_OPT, CLUSTER_DOMAIN_SECRET_OPT], + NEW_CLUSTER_DOMAIN_SECRET_OPT, CLUSTER_DOMAIN_SECRET_OPT, + NEW_SPICE_CERT_OPT, SPICE_CERT_OPT, SPICE_CACERT_OPT], "[opts...]", "Renews cluster certificates, keys and secrets"), "epo": ( diff --git a/lib/errors.py b/lib/errors.py index 181288618..ff7cbf851 100644 --- a/lib/errors.py +++ b/lib/errors.py @@ -277,6 +277,14 @@ class SshKeyError(GenericError): """ +class X509CertError(GenericError): + """Invalid X509 certificate. + + This error has two arguments: the certificate filename and the error cause. + + """ + + class TagError(GenericError): """Generic tag error. diff --git a/lib/hypervisor/hv_kvm.py b/lib/hypervisor/hv_kvm.py index 754efb4d7..ad7cc2d33 100644 --- a/lib/hypervisor/hv_kvm.py +++ b/lib/hypervisor/hv_kvm.py @@ -794,7 +794,7 @@ class KVMHypervisor(hv_base.BaseHypervisor): """Generate KVM information to start an instance. """ - # pylint: disable=R0914 + # pylint: disable=R0914,R0915 _, v_major, v_min, _ = self._GetKVMVersion() pidfile = self._InstancePidFile(instance.name) diff --git a/lib/utils/x509.py b/lib/utils/x509.py index 71ba25dc3..b0d9f904c 100644 --- a/lib/utils/x509.py +++ b/lib/utils/x509.py @@ -259,6 +259,8 @@ def GenerateSelfSignedX509Cert(common_name, validity): @param common_name: commonName value @type validity: int @param validity: Validity for certificate in seconds + @return: a tuple of strings containing the PEM-encoded private key and + certificate """ # Create private and public key @@ -292,6 +294,8 @@ def GenerateSelfSignedSslCert(filename, common_name=constants.X509_CERT_CN, @param common_name: commonName value @type validity: int @param validity: validity of certificate in number of days + @return: a tuple of strings containing the PEM-encoded private key and + certificate """ # TODO: Investigate using the cluster name instead of X505_CERT_CN for @@ -301,3 +305,4 @@ def GenerateSelfSignedSslCert(filename, common_name=constants.X509_CERT_CN, validity * 24 * 60 * 60) utils_io.WriteFile(filename, mode=0400, data=key_pem + cert_pem) + return (key_pem, cert_pem) diff --git a/tools/cfgupgrade b/tools/cfgupgrade index b44ea6c1f..882dbfbdc 100755 --- a/tools/cfgupgrade +++ b/tools/cfgupgrade @@ -122,6 +122,8 @@ def main(): options.SERVER_PEM_PATH = options.data_dir + "/server.pem" options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts" options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem" + options.SPICE_CERT_FILE = options.data_dir + "/spice.pem" + options.SPICE_CACERT_FILE = options.data_dir + "/spice-ca.pem" options.RAPI_USERS_FILE = options.data_dir + "/rapi/users" options.RAPI_USERS_FILE_PRE24 = options.data_dir + "/rapi_users" options.CONFD_HMAC_KEY = options.data_dir + "/hmac.key" @@ -222,11 +224,13 @@ def main(): backup=True) if not options.dry_run: - bootstrap.GenerateClusterCrypto(False, False, False, False, - nodecert_file=options.SERVER_PEM_PATH, - rapicert_file=options.RAPI_CERT_FILE, - hmackey_file=options.CONFD_HMAC_KEY, - cds_file=options.CDS_FILE) + bootstrap.GenerateClusterCrypto(False, False, False, False, False, + nodecert_file=options.SERVER_PEM_PATH, + rapicert_file=options.RAPI_CERT_FILE, + spicecert_file=options.SPICE_CERT_FILE, + spicecacert_file=options.SPICE_CACERT_FILE, + hmackey_file=options.CONFD_HMAC_KEY, + cds_file=options.CDS_FILE) except Exception: logging.critical("Writing configuration failed. It is probably in an" -- GitLab