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):
action)
if token_type == constants.CRYPTO_TYPE_SSL_DIGEST:
if action == constants.CRYPTO_ACTION_CREATE:
# extract file name from options
cert_filename = None
if options:
cert_filename = options.get(constants.CRYPTO_OPTION_CERT_FILE)
......@@ -1216,8 +1218,25 @@ def GetCryptoTokens(token_requests):
raise errors.ProgrammerError(
"The certificate file name path '%s' is not allowed." %
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(
True, cert_filename,
True, cert_filename, serial_no,
"Create new client SSL certificate in %s." % cert_filename)
tokens.append((token_type,
utils.GetCertificateDigest(
......@@ -3684,7 +3703,7 @@ def CreateX509Certificate(validity, cryptodir=pathutils.CRYPTO_KEYS_DIR):
"""
(key_pem, cert_pem) = \
utils.GenerateSelfSignedX509Cert(netutils.Hostname.GetSysName(),
min(validity, _MAX_SSL_CERT_VALIDITY))
min(validity, _MAX_SSL_CERT_VALIDITY), 1)
cert_dir = tempfile.mkdtemp(dir=cryptodir,
prefix="x509-%s-" % utils.TimestampForFilename())
......
......@@ -138,7 +138,7 @@ def GenerateClusterCrypto(new_cluster_cert, new_rapi_cert, new_spice_cert,
# pylint: disable=R0913
# noded SSL certificate
utils.GenerateNewSslCert(
new_cluster_cert, nodecert_file,
new_cluster_cert, nodecert_file, 1,
"Generating new cluster certificate at %s" % nodecert_file)
# confd HMAC key
......@@ -153,7 +153,7 @@ def GenerateClusterCrypto(new_cluster_cert, new_rapi_cert, new_spice_cert,
else:
utils.GenerateNewSslCert(
new_rapi_cert, rapicert_file,
new_rapi_cert, rapicert_file, 1,
"Generating new RAPI certificate at %s" % rapicert_file)
# SPICE
......@@ -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",
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
# certificate
......
......@@ -1268,6 +1268,7 @@ def CreateNewClientCert(lu, node_uuid, filename=None):
options = {}
if 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(
node_uuid,
[(constants.CRYPTO_TYPE_SSL_DIGEST,
......
......@@ -25,6 +25,7 @@
import logging
import OpenSSL
import os
import uuid as uuid_module
from ganeti.utils import io
from ganeti.utils import x509
......@@ -33,6 +34,11 @@ from ganeti import errors
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,
info_fn=logging.info, warn_fn=logging.warn):
"""Adds an entry to the candidate certificate map.
......@@ -94,13 +100,15 @@ def GetCertificateDigest(cert_filename=pathutils.NODED_CLIENT_CERT_FILE):
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.
@type new_cert: boolean
@param new_cert: whether a new certificate should be created
@type cert_filename: string
@param cert_filename: filename of the certificate file
@type serial_no: int
@param serial_no: serial number of the certificate
@type log_msg: string
@param log_msg: log message to be written on certificate creation
......@@ -111,7 +119,7 @@ def GenerateNewSslCert(new_cert, cert_filename, log_msg):
io.CreateBackup(cert_filename)
logging.debug(log_msg)
x509.GenerateSelfSignedSslCert(cert_filename)
x509.GenerateSelfSignedSslCert(cert_filename, serial_no)
def VerifyCertificate(filename):
......
......@@ -254,7 +254,7 @@ def LoadSignedX509Certificate(cert_pem, key):
return (cert, salt)
def GenerateSelfSignedX509Cert(common_name, validity):
def GenerateSelfSignedX509Cert(common_name, validity, serial_no):
"""Generates a self-signed X509 certificate.
@type common_name: string
......@@ -273,7 +273,7 @@ def GenerateSelfSignedX509Cert(common_name, validity):
cert = OpenSSL.crypto.X509()
if 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_notAfter(validity)
cert.set_issuer(cert.get_subject())
......@@ -286,7 +286,8 @@ def GenerateSelfSignedX509Cert(common_name, validity):
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):
"""Legacy function to generate self-signed X509 certificate.
......@@ -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
# common_name, as cluster-renames are very seldom, and it'd be nice if RAPI
# and node daemon certificates have the proper Subject/Issuer.
(key_pem, cert_pem) = GenerateSelfSignedX509Cert(common_name,
validity * 24 * 60 * 60)
(key_pem, cert_pem) = GenerateSelfSignedX509Cert(
common_name, validity * 24 * 60 * 60, serial_no)
utils_io.WriteFile(filename, mode=0400, data=key_pem + cert_pem)
return (key_pem, cert_pem)
......
......@@ -1049,7 +1049,7 @@ def TestClusterRenewCrypto():
# Ensure certificate doesn't cause "gnt-cluster verify" to complain
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)
try:
......
......@@ -4207,6 +4207,10 @@ cryptoActions = ConstantUtils.mkSet [cryptoActionGet, cryptoActionCreate]
cryptoOptionCertFile :: String
cryptoOptionCertFile = "cert_file"
-- Serial number of the certificate
cryptoOptionSerialNo :: String
cryptoOptionSerialNo = "serial_no"
-- * SSH key types
sshkDsa :: String
......
......@@ -97,7 +97,7 @@ class TestGetCryptoTokens(testutils.GanetiTestCase):
def testCreateSslToken(self):
result = backend.GetCryptoTokens(
[(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)
in result)
self.assertTrue(utils.GenerateNewSslCert.assert_calls().once())
......@@ -106,7 +106,16 @@ class TestGetCryptoTokens(testutils.GanetiTestCase):
result = backend.GetCryptoTokens(
[(constants.CRYPTO_TYPE_SSL_DIGEST, constants.CRYPTO_ACTION_CREATE,
{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)
in result)
self.assertTrue(utils.GenerateNewSslCert.assert_calls().once())
......
......@@ -33,6 +33,13 @@ from ganeti.utils import security
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):
def setUp(self):
......
......@@ -96,7 +96,7 @@ class TestSignX509Certificate(unittest.TestCase):
def test(self):
# 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_pem)
......@@ -257,7 +257,8 @@ class TestGenerateSelfSignedX509Cert(unittest.TestCase):
def test(self):
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._checkCertificate(cert_pem)
......@@ -277,7 +278,7 @@ class TestGenerateSelfSignedX509Cert(unittest.TestCase):
def testLegacy(self):
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)
......
......@@ -97,7 +97,7 @@ def main():
elif what == "connected":
WaitForConnected(filename)
elif what == "gencert":
utils.GenerateSelfSignedSslCert(filename, validity=VALIDITY)
utils.GenerateSelfSignedSslCert(filename, 1, validity=VALIDITY)
else:
raise Exception("Unknown command '%s'" % what)
......
......@@ -405,7 +405,7 @@ def main():
if not options.dry_run:
if not os.path.exists(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:
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