Commit b544a3c2 authored by Helga Velroyen's avatar Helga Velroyen
Browse files

Retrieve a node's certificate digest

In various cluster operations, the master node needs to
retrieve the digest of a node's SSL certificate. For this
purpose, we add an RPC call to retrieve the digest. The
function is designed in a general way to make it possible
to retrieve other (public) cryptographic tokens of a node
in the future as well (for example an SSH key).
Signed-off-by: default avatarHelga Velroyen <>
Reviewed-by: default avatarHrvoje Ribicic <>
parent 3338a9ce
......@@ -1162,6 +1162,30 @@ def VerifyNode(what, cluster_name, all_hvparams, node_groups, groups_cfg):
return result
def GetCryptoTokens(token_types):
"""Get the node's public cryptographic tokens.
This can be the public ssh key of the node or the certificate digest of
the node's public client SSL certificate.
Note: so far, only retrieval of the SSL digest is implemented
@type token_types: list of strings in constants.CRYPTO_TYPES
@param token_types: list of types of requested cryptographic tokens
@rtype: list of tuples (string, string)
@return: list of tuples of the token type and the public crypto token
tokens = []
for token_type in token_types:
if token_type not in constants.CRYPTO_TYPES:
raise errors.ProgrammerError("Token type %s not supported." %
if token_type == constants.CRYPTO_TYPE_SSL_DIGEST:
tokens.append((token_type, utils.GetClientCertificateDigest()))
return tokens
def GetBlockDevSizes(devices):
"""Return the size of the given block devices
......@@ -505,6 +505,9 @@ _NODE_CALLS = [
("ovs_name", None, "Name of the OpenvSwitch to create"),
("ovs_link", None, "Link of the OpenvSwitch to the outside"),
], None, None, "This will create and setup the OpenvSwitch"),
("node_crypto_tokens", SINGLE, None, constants.RPC_TMO_NORMAL, [
("token_types", None, "List of requested crypto token types"),
], None, None, "Retrieve public crypto tokens from the node."),
......@@ -864,6 +864,14 @@ class NodeRequestHandler(http.server.HttpServerHandler):
(ovs_name, ovs_link) = params
return backend.ConfigureOVS(ovs_name, ovs_link)
def perspective_node_crypto_tokens(params):
"""Gets the node's public crypto tokens.
token_types = params[0]
return backend.GetCryptoTokens(token_types)
# cluster --------------------------
......@@ -53,6 +53,7 @@ from ganeti.utils.mlock import *
from ganeti.utils.nodesetup import *
from ganeti.utils.process import *
from ganeti.utils.retry import *
from import *
from import *
from ganeti.utils.text import *
from ganeti.utils.wrapper import *
......@@ -23,6 +23,10 @@
import logging
import OpenSSL
from ganeti.utils import io
from ganeti import pathutils
def AddNodeToCandidateCerts(node_uuid, cert_digest, candidate_certs,
......@@ -74,3 +78,17 @@ def RemoveNodeFromCandidateCerts(node_uuid, candidate_certs,
"candidate map." % node_uuid)
del candidate_certs[node_uuid]
def GetClientCertificateDigest(cert_filename=pathutils.NODED_CERT_FILE):
"""Reads the SSL certificate and returns the sha1 digest.
# FIXME: This is supposed to read the client certificate, but
# in this stage of the patch series there is no client certificate
# yet, so we return the digest of the server certificate to get
# the rest of the key management infrastructure running.
cert_plain = io.ReadFile(cert_filename)
cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM,
return cert.digest("sha1")
......@@ -4140,6 +4140,21 @@ fakeOpMasterTurndown = "OP_CLUSTER_IP_TURNDOWN"
fakeOpMasterTurnup :: String
fakeOpMasterTurnup = "OP_CLUSTER_IP_TURNUP"
-- * Crypto Types
-- Types of cryptographic tokens used in node communication
cryptoTypeSslDigest :: String
cryptoTypeSslDigest = "ssl"
cryptoTypeSsh :: String
cryptoTypeSsh = "ssh"
-- So far only ssl keys are used in the context of this constant
cryptoTypes :: FrozenSet String
cryptoTypes = ConstantUtils.mkSet [cryptoTypeSslDigest]
-- * SSH key types
sshkDsa :: String
......@@ -73,6 +73,27 @@ class TestX509Certificates(unittest.TestCase):
self.assertEqual(utils.ListVisibleFiles(self.tmpdir), [name])
class TestGetCryptoTokens(testutils.GanetiTestCase):
def setUp(self):
self._digest_fn_orig = utils.GetClientCertificateDigest
self._ssl_digest = "12345"
utils.GetClientCertificateDigest = mock.Mock(
def tearDown(self):
utils.GetClientCertificateDigest = self._digest_fn_orig
def testSslToken(self):
result = backend.GetCryptoTokens([constants.CRYPTO_TYPE_SSL_DIGEST])
self.assertTrue((constants.CRYPTO_TYPE_SSL_DIGEST, self._ssl_digest)
in result)
def testUnknownToken(self):
backend.GetCryptoTokens, ["pink_bunny"])
class TestNodeVerify(testutils.GanetiTestCase):
def setUp(self):
......@@ -70,5 +70,23 @@ class TestCandidateCerts(unittest.TestCase):
self.assertEqual(0, len(self._candidate_certs))
class TestGetCertificateDigest(testutils.GanetiTestCase):
def setUp(self):
# certificate file that contains the certificate only
self._certfilename1 = testutils.TestDataFilename("cert1.pem")
# (different) certificate file that contains both, certificate
# and private key
self._certfilename2 = testutils.TestDataFilename("cert2.pem")
def testGetCertificateDigest(self):
digest1 = security.GetClientCertificateDigest(
digest2 = security.GetClientCertificateDigest(
self.assertFalse(digest1 == digest2)
if __name__ == "__main__":
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