Commit ab4b1cf2 authored by Helga Velroyen's avatar Helga Velroyen

Use node UUID as client certificate serial number

It turns out, that some implementations of OpenSSL are more
pedantic in checking the certficates than others. In this
particular case, the SSL connection could not be
established when the serial number of the certificates
was not unique.

To avoid this problem, this patch extends Ganeti's X509
infrastructure to set the certificate's serial
number. In case of client certificates, we now use the
node's UUID as serial number, because the UUIDs are
assumed to be unique in a cluster. This is however still
not complying to how SSL was designed to be used, but at
least it is a lot better than setting every serial number
to 1, which was used before and is still used for other
certificates than the client certificate.
Signed-off-by: default avatarHelga Velroyen <helgav@google.com>
Reviewed-by: default avatarKlaus Aehlig <aehlig@google.com>
parent a15cd685
...@@ -1206,6 +1206,8 @@ def GetCryptoTokens(token_requests): ...@@ -1206,6 +1206,8 @@ def GetCryptoTokens(token_requests):
action) action)
if token_type == constants.CRYPTO_TYPE_SSL_DIGEST: if token_type == constants.CRYPTO_TYPE_SSL_DIGEST:
if action == constants.CRYPTO_ACTION_CREATE: if action == constants.CRYPTO_ACTION_CREATE:
# extract file name from options
cert_filename = None cert_filename = None
if options: if options:
cert_filename = options.get(constants.CRYPTO_OPTION_CERT_FILE) cert_filename = options.get(constants.CRYPTO_OPTION_CERT_FILE)
...@@ -1216,8 +1218,25 @@ def GetCryptoTokens(token_requests): ...@@ -1216,8 +1218,25 @@ def GetCryptoTokens(token_requests):
raise errors.ProgrammerError( raise errors.ProgrammerError(
"The certificate file name path '%s' is not allowed." % "The certificate file name path '%s' is not allowed." %
cert_filename) cert_filename)
# extract serial number from options
serial_no = None
if options:
try:
serial_no = int(options[constants.CRYPTO_OPTION_SERIAL_NO])
except ValueError:
raise errors.ProgrammerError(
"The given serial number is not an intenger: %s." %
options.get(constants.CRYPTO_OPTION_SERIAL_NO))
except KeyError:
raise errors.ProgrammerError("No serial number was provided.")
if not serial_no:
raise errors.ProgrammerError(
"Cannot create an SSL certificate without a serial no.")
utils.GenerateNewSslCert( utils.GenerateNewSslCert(
True, cert_filename, True, cert_filename, serial_no,
"Create new client SSL certificate in %s." % cert_filename) "Create new client SSL certificate in %s." % cert_filename)
tokens.append((token_type, tokens.append((token_type,
utils.GetCertificateDigest( utils.GetCertificateDigest(
...@@ -3684,7 +3703,7 @@ def CreateX509Certificate(validity, cryptodir=pathutils.CRYPTO_KEYS_DIR): ...@@ -3684,7 +3703,7 @@ def CreateX509Certificate(validity, cryptodir=pathutils.CRYPTO_KEYS_DIR):
""" """
(key_pem, cert_pem) = \ (key_pem, cert_pem) = \
utils.GenerateSelfSignedX509Cert(netutils.Hostname.GetSysName(), utils.GenerateSelfSignedX509Cert(netutils.Hostname.GetSysName(),
min(validity, _MAX_SSL_CERT_VALIDITY)) min(validity, _MAX_SSL_CERT_VALIDITY), 1)
cert_dir = tempfile.mkdtemp(dir=cryptodir, cert_dir = tempfile.mkdtemp(dir=cryptodir,
prefix="x509-%s-" % utils.TimestampForFilename()) prefix="x509-%s-" % utils.TimestampForFilename())
......
...@@ -138,7 +138,7 @@ def GenerateClusterCrypto(new_cluster_cert, new_rapi_cert, new_spice_cert, ...@@ -138,7 +138,7 @@ def GenerateClusterCrypto(new_cluster_cert, new_rapi_cert, new_spice_cert,
# pylint: disable=R0913 # pylint: disable=R0913
# noded SSL certificate # noded SSL certificate
utils.GenerateNewSslCert( utils.GenerateNewSslCert(
new_cluster_cert, nodecert_file, new_cluster_cert, nodecert_file, 1,
"Generating new cluster certificate at %s" % nodecert_file) "Generating new cluster certificate at %s" % nodecert_file)
# confd HMAC key # confd HMAC key
...@@ -153,7 +153,7 @@ def GenerateClusterCrypto(new_cluster_cert, new_rapi_cert, new_spice_cert, ...@@ -153,7 +153,7 @@ def GenerateClusterCrypto(new_cluster_cert, new_rapi_cert, new_spice_cert,
else: else:
utils.GenerateNewSslCert( utils.GenerateNewSslCert(
new_rapi_cert, rapicert_file, new_rapi_cert, rapicert_file, 1,
"Generating new RAPI certificate at %s" % rapicert_file) "Generating new RAPI certificate at %s" % rapicert_file)
# SPICE # SPICE
...@@ -173,7 +173,7 @@ def GenerateClusterCrypto(new_cluster_cert, new_rapi_cert, new_spice_cert, ...@@ -173,7 +173,7 @@ def GenerateClusterCrypto(new_cluster_cert, new_rapi_cert, new_spice_cert,
logging.debug("Generating new self-signed SPICE certificate at %s", logging.debug("Generating new self-signed SPICE certificate at %s",
spicecert_file) spicecert_file)
(_, cert_pem) = utils.GenerateSelfSignedSslCert(spicecert_file) (_, cert_pem) = utils.GenerateSelfSignedSslCert(spicecert_file, 1)
# Self-signed certificate -> the public certificate is also the CA public # Self-signed certificate -> the public certificate is also the CA public
# certificate # certificate
......
...@@ -1268,6 +1268,7 @@ def CreateNewClientCert(lu, node_uuid, filename=None): ...@@ -1268,6 +1268,7 @@ def CreateNewClientCert(lu, node_uuid, filename=None):
options = {} options = {}
if filename: if filename:
options[constants.CRYPTO_OPTION_CERT_FILE] = filename options[constants.CRYPTO_OPTION_CERT_FILE] = filename
options[constants.CRYPTO_OPTION_SERIAL_NO] = utils.UuidToInt(node_uuid)
result = lu.rpc.call_node_crypto_tokens( result = lu.rpc.call_node_crypto_tokens(
node_uuid, node_uuid,
[(constants.CRYPTO_TYPE_SSL_DIGEST, [(constants.CRYPTO_TYPE_SSL_DIGEST,
......
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
import logging import logging
import OpenSSL import OpenSSL
import os import os
import uuid as uuid_module
from ganeti.utils import io from ganeti.utils import io
from ganeti.utils import x509 from ganeti.utils import x509
...@@ -33,6 +34,11 @@ from ganeti import errors ...@@ -33,6 +34,11 @@ from ganeti import errors
from ganeti import pathutils from ganeti import pathutils
def UuidToInt(uuid):
uuid_obj = uuid_module.UUID(uuid)
return uuid_obj.int # pylint: disable=E1101
def AddNodeToCandidateCerts(node_uuid, cert_digest, candidate_certs, def AddNodeToCandidateCerts(node_uuid, cert_digest, candidate_certs,
info_fn=logging.info, warn_fn=logging.warn): info_fn=logging.info, warn_fn=logging.warn):
"""Adds an entry to the candidate certificate map. """Adds an entry to the candidate certificate map.
...@@ -94,13 +100,15 @@ def GetCertificateDigest(cert_filename=pathutils.NODED_CLIENT_CERT_FILE): ...@@ -94,13 +100,15 @@ def GetCertificateDigest(cert_filename=pathutils.NODED_CLIENT_CERT_FILE):
return cert.digest("sha1") return cert.digest("sha1")
def GenerateNewSslCert(new_cert, cert_filename, log_msg): def GenerateNewSslCert(new_cert, cert_filename, serial_no, log_msg):
"""Creates a new SSL certificate and backups the old one. """Creates a new SSL certificate and backups the old one.
@type new_cert: boolean @type new_cert: boolean
@param new_cert: whether a new certificate should be created @param new_cert: whether a new certificate should be created
@type cert_filename: string @type cert_filename: string
@param cert_filename: filename of the certificate file @param cert_filename: filename of the certificate file
@type serial_no: int
@param serial_no: serial number of the certificate
@type log_msg: string @type log_msg: string
@param log_msg: log message to be written on certificate creation @param log_msg: log message to be written on certificate creation
...@@ -111,7 +119,7 @@ def GenerateNewSslCert(new_cert, cert_filename, log_msg): ...@@ -111,7 +119,7 @@ def GenerateNewSslCert(new_cert, cert_filename, log_msg):
io.CreateBackup(cert_filename) io.CreateBackup(cert_filename)
logging.debug(log_msg) logging.debug(log_msg)
x509.GenerateSelfSignedSslCert(cert_filename) x509.GenerateSelfSignedSslCert(cert_filename, serial_no)
def VerifyCertificate(filename): def VerifyCertificate(filename):
......
...@@ -254,7 +254,7 @@ def LoadSignedX509Certificate(cert_pem, key): ...@@ -254,7 +254,7 @@ def LoadSignedX509Certificate(cert_pem, key):
return (cert, salt) return (cert, salt)
def GenerateSelfSignedX509Cert(common_name, validity): def GenerateSelfSignedX509Cert(common_name, validity, serial_no):
"""Generates a self-signed X509 certificate. """Generates a self-signed X509 certificate.
@type common_name: string @type common_name: string
...@@ -273,7 +273,7 @@ def GenerateSelfSignedX509Cert(common_name, validity): ...@@ -273,7 +273,7 @@ def GenerateSelfSignedX509Cert(common_name, validity):
cert = OpenSSL.crypto.X509() cert = OpenSSL.crypto.X509()
if common_name: if common_name:
cert.get_subject().CN = common_name cert.get_subject().CN = common_name
cert.set_serial_number(1) cert.set_serial_number(serial_no)
cert.gmtime_adj_notBefore(0) cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notAfter(validity) cert.gmtime_adj_notAfter(validity)
cert.set_issuer(cert.get_subject()) cert.set_issuer(cert.get_subject())
...@@ -286,7 +286,8 @@ def GenerateSelfSignedX509Cert(common_name, validity): ...@@ -286,7 +286,8 @@ def GenerateSelfSignedX509Cert(common_name, validity):
return (key_pem, cert_pem) return (key_pem, cert_pem)
def GenerateSelfSignedSslCert(filename, common_name=constants.X509_CERT_CN, def GenerateSelfSignedSslCert(filename, serial_no,
common_name=constants.X509_CERT_CN,
validity=constants.X509_CERT_DEFAULT_VALIDITY): validity=constants.X509_CERT_DEFAULT_VALIDITY):
"""Legacy function to generate self-signed X509 certificate. """Legacy function to generate self-signed X509 certificate.
...@@ -303,8 +304,8 @@ def GenerateSelfSignedSslCert(filename, common_name=constants.X509_CERT_CN, ...@@ -303,8 +304,8 @@ def GenerateSelfSignedSslCert(filename, common_name=constants.X509_CERT_CN,
# TODO: Investigate using the cluster name instead of X505_CERT_CN for # TODO: Investigate using the cluster name instead of X505_CERT_CN for
# common_name, as cluster-renames are very seldom, and it'd be nice if RAPI # common_name, as cluster-renames are very seldom, and it'd be nice if RAPI
# and node daemon certificates have the proper Subject/Issuer. # and node daemon certificates have the proper Subject/Issuer.
(key_pem, cert_pem) = GenerateSelfSignedX509Cert(common_name, (key_pem, cert_pem) = GenerateSelfSignedX509Cert(
validity * 24 * 60 * 60) common_name, validity * 24 * 60 * 60, serial_no)
utils_io.WriteFile(filename, mode=0400, data=key_pem + cert_pem) utils_io.WriteFile(filename, mode=0400, data=key_pem + cert_pem)
return (key_pem, cert_pem) return (key_pem, cert_pem)
......
...@@ -1049,7 +1049,7 @@ def TestClusterRenewCrypto(): ...@@ -1049,7 +1049,7 @@ def TestClusterRenewCrypto():
# Ensure certificate doesn't cause "gnt-cluster verify" to complain # Ensure certificate doesn't cause "gnt-cluster verify" to complain
validity = constants.SSL_CERT_EXPIRATION_WARN * 3 validity = constants.SSL_CERT_EXPIRATION_WARN * 3
utils.GenerateSelfSignedSslCert(fh.name, validity=validity) utils.GenerateSelfSignedSslCert(fh.name, 1, validity=validity)
tmpcert = qa_utils.UploadFile(master.primary, fh.name) tmpcert = qa_utils.UploadFile(master.primary, fh.name)
try: try:
......
...@@ -4207,6 +4207,10 @@ cryptoActions = ConstantUtils.mkSet [cryptoActionGet, cryptoActionCreate] ...@@ -4207,6 +4207,10 @@ cryptoActions = ConstantUtils.mkSet [cryptoActionGet, cryptoActionCreate]
cryptoOptionCertFile :: String cryptoOptionCertFile :: String
cryptoOptionCertFile = "cert_file" cryptoOptionCertFile = "cert_file"
-- Serial number of the certificate
cryptoOptionSerialNo :: String
cryptoOptionSerialNo = "serial_no"
-- * SSH key types -- * SSH key types
sshkDsa :: String sshkDsa :: String
......
...@@ -97,7 +97,7 @@ class TestGetCryptoTokens(testutils.GanetiTestCase): ...@@ -97,7 +97,7 @@ class TestGetCryptoTokens(testutils.GanetiTestCase):
def testCreateSslToken(self): def testCreateSslToken(self):
result = backend.GetCryptoTokens( result = backend.GetCryptoTokens(
[(constants.CRYPTO_TYPE_SSL_DIGEST, constants.CRYPTO_ACTION_CREATE, [(constants.CRYPTO_TYPE_SSL_DIGEST, constants.CRYPTO_ACTION_CREATE,
None)]) {constants.CRYPTO_OPTION_SERIAL_NO: 42})])
self.assertTrue((constants.CRYPTO_TYPE_SSL_DIGEST, self._ssl_digest) self.assertTrue((constants.CRYPTO_TYPE_SSL_DIGEST, self._ssl_digest)
in result) in result)
self.assertTrue(utils.GenerateNewSslCert.assert_calls().once()) self.assertTrue(utils.GenerateNewSslCert.assert_calls().once())
...@@ -106,7 +106,16 @@ class TestGetCryptoTokens(testutils.GanetiTestCase): ...@@ -106,7 +106,16 @@ class TestGetCryptoTokens(testutils.GanetiTestCase):
result = backend.GetCryptoTokens( result = backend.GetCryptoTokens(
[(constants.CRYPTO_TYPE_SSL_DIGEST, constants.CRYPTO_ACTION_CREATE, [(constants.CRYPTO_TYPE_SSL_DIGEST, constants.CRYPTO_ACTION_CREATE,
{constants.CRYPTO_OPTION_CERT_FILE: {constants.CRYPTO_OPTION_CERT_FILE:
pathutils.NODED_CLIENT_CERT_FILE_TMP})]) pathutils.NODED_CLIENT_CERT_FILE_TMP,
constants.CRYPTO_OPTION_SERIAL_NO: 42})])
self.assertTrue((constants.CRYPTO_TYPE_SSL_DIGEST, self._ssl_digest)
in result)
self.assertTrue(utils.GenerateNewSslCert.assert_calls().once())
def testCreateSslTokenSerialNo(self):
result = backend.GetCryptoTokens(
[(constants.CRYPTO_TYPE_SSL_DIGEST, constants.CRYPTO_ACTION_CREATE,
{constants.CRYPTO_OPTION_SERIAL_NO: 42})])
self.assertTrue((constants.CRYPTO_TYPE_SSL_DIGEST, self._ssl_digest) self.assertTrue((constants.CRYPTO_TYPE_SSL_DIGEST, self._ssl_digest)
in result) in result)
self.assertTrue(utils.GenerateNewSslCert.assert_calls().once()) self.assertTrue(utils.GenerateNewSslCert.assert_calls().once())
......
...@@ -33,6 +33,13 @@ from ganeti.utils import security ...@@ -33,6 +33,13 @@ from ganeti.utils import security
import testutils import testutils
class TestUuidConversion(unittest.TestCase):
def testUuidConversion(self):
uuid_as_int = security.UuidToInt("5cd037f4-9587-49c4-a23e-142f8b7e909d")
self.assertEqual(uuid_as_int, int(uuid_as_int))
class TestCandidateCerts(unittest.TestCase): class TestCandidateCerts(unittest.TestCase):
def setUp(self): def setUp(self):
......
...@@ -96,7 +96,7 @@ class TestSignX509Certificate(unittest.TestCase): ...@@ -96,7 +96,7 @@ class TestSignX509Certificate(unittest.TestCase):
def test(self): def test(self):
# Generate certificate valid for 5 minutes # Generate certificate valid for 5 minutes
(_, cert_pem) = utils.GenerateSelfSignedX509Cert(None, 300) (_, cert_pem) = utils.GenerateSelfSignedX509Cert(None, 300, 1)
cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM,
cert_pem) cert_pem)
...@@ -257,7 +257,8 @@ class TestGenerateSelfSignedX509Cert(unittest.TestCase): ...@@ -257,7 +257,8 @@ class TestGenerateSelfSignedX509Cert(unittest.TestCase):
def test(self): def test(self):
for common_name in [None, ".", "Ganeti", "node1.example.com"]: for common_name in [None, ".", "Ganeti", "node1.example.com"]:
(key_pem, cert_pem) = utils.GenerateSelfSignedX509Cert(common_name, 300) (key_pem, cert_pem) = utils.GenerateSelfSignedX509Cert(common_name, 300,
1)
self._checkRsaPrivateKey(key_pem) self._checkRsaPrivateKey(key_pem)
self._checkCertificate(cert_pem) self._checkCertificate(cert_pem)
...@@ -277,7 +278,7 @@ class TestGenerateSelfSignedX509Cert(unittest.TestCase): ...@@ -277,7 +278,7 @@ class TestGenerateSelfSignedX509Cert(unittest.TestCase):
def testLegacy(self): def testLegacy(self):
cert1_filename = os.path.join(self.tmpdir, "cert1.pem") cert1_filename = os.path.join(self.tmpdir, "cert1.pem")
utils.GenerateSelfSignedSslCert(cert1_filename, validity=1) utils.GenerateSelfSignedSslCert(cert1_filename, 1, validity=1)
cert1 = utils.ReadFile(cert1_filename) cert1 = utils.ReadFile(cert1_filename)
......
...@@ -97,7 +97,7 @@ def main(): ...@@ -97,7 +97,7 @@ def main():
elif what == "connected": elif what == "connected":
WaitForConnected(filename) WaitForConnected(filename)
elif what == "gencert": elif what == "gencert":
utils.GenerateSelfSignedSslCert(filename, validity=VALIDITY) utils.GenerateSelfSignedSslCert(filename, 1, validity=VALIDITY)
else: else:
raise Exception("Unknown command '%s'" % what) raise Exception("Unknown command '%s'" % what)
......
...@@ -405,7 +405,7 @@ def main(): ...@@ -405,7 +405,7 @@ def main():
if not options.dry_run: if not options.dry_run:
if not os.path.exists(options.RAPI_CERT_FILE): if not os.path.exists(options.RAPI_CERT_FILE):
logging.debug("Writing RAPI certificate to %s", options.RAPI_CERT_FILE) logging.debug("Writing RAPI certificate to %s", options.RAPI_CERT_FILE)
utils.GenerateSelfSignedSslCert(options.RAPI_CERT_FILE) utils.GenerateSelfSignedSslCert(options.RAPI_CERT_FILE, 1)
except: except:
logging.critical("Writing configuration failed. It is probably in an" logging.critical("Writing configuration failed. It is probably in an"
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment