Implementation of TLS-protected SPICE connections
authorAndrea Spadaccini <spadaccio@google.com>
Tue, 6 Sep 2011 17:14:51 +0000 (18:14 +0100)
committerAndrea Spadaccini <spadaccio@google.com>
Tue, 20 Sep 2011 14:04:46 +0000 (15:04 +0100)
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
lib/cli.py
lib/client/gnt_cluster.py
lib/errors.py
lib/hypervisor/hv_kvm.py
lib/utils/x509.py
tools/cfgupgrade

index a058c56..85c2251 100644 (file)
@@ -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:
index d7e497e..3f8d497 100644 (file)
@@ -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",
index b61e8e3..5637774 100644 (file)
@@ -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": (
index 1812886..ff7cbf8 100644 (file)
@@ -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.
 
index 754efb4..ad7cc2d 100644 (file)
@@ -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)
index 71ba25d..b0d9f90 100644 (file)
@@ -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)
index b44ea6c..882dbfb 100755 (executable)
@@ -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"