994 lines
37 KiB
Diff
994 lines
37 KiB
Diff
Based on #61737
|
|
Fixes (workaround): https://tracker.ceph.com/issues/64213
|
|
|
|
This PR takes some of the work done by @pecastro along with suggestions by
|
|
@epuertat and combines them into a series of patches that create two
|
|
implementations with a common interface (via ABCs) for calling basic
|
|
cryptographic and TSL/SSL related functionality. This interface is named
|
|
CryptoCaller.
|
|
|
|
The new 'remote' implementation uses subprocesses to execute the cryptography
|
|
functions outside of the mgr. This avoids conflicts between the pyo3 using libs
|
|
(specifically cryptography) across sub-interpreters. The 'internal'
|
|
implementation uses the library calls directly but does so through a class
|
|
based interface. A helper module allows mgr code to choose or get the currently
|
|
enabled (per sub-interpreter) crypto caller implementation.
|
|
|
|
The mgr utils are updated to use this interface. The cephadm module uses the
|
|
internal implemenation. The dashboard defaults to the remote implementation but
|
|
can be configured.
|
|
--- a/src/pybind/mgr/cephadm/module.py
|
|
+++ b/src/pybind/mgr/cephadm/module.py
|
|
@@ -37,6 +37,7 @@ from ceph.deployment.service_spec import
|
|
HostPlacementSpec, IngressSpec, \
|
|
TunedProfileSpec, IscsiServiceSpec
|
|
from ceph.utils import str_to_datetime, datetime_to_str, datetime_now
|
|
+from ceph.cryptotools.select import choose_crypto_caller
|
|
from cephadm.serve import CephadmServe
|
|
from cephadm.services.cephadmservice import CephadmDaemonDeploySpec
|
|
from cephadm.http_server import CephadmHttpServer
|
|
@@ -502,6 +503,10 @@ class CephadmOrchestrator(orchestrator.O
|
|
super(CephadmOrchestrator, self).__init__(*args, **kwargs)
|
|
self._cluster_fsid: str = self.get('mon_map')['fsid']
|
|
self.last_monmap: Optional[datetime.datetime] = None
|
|
+ # cephadm module always needs access to the real cryptography module
|
|
+ # for asyncssh. It is always permitted to use the internal
|
|
+ # cryptocaller.
|
|
+ choose_crypto_caller('internal')
|
|
|
|
# for serve()
|
|
self.run = True
|
|
--- a/src/pybind/mgr/cephadm/tests/test_cephadm.py
|
|
+++ b/src/pybind/mgr/cephadm/tests/test_cephadm.py
|
|
@@ -1830,12 +1830,10 @@ class TestCephadm(object):
|
|
), CephadmOrchestrator.apply_container),
|
|
]
|
|
)
|
|
- @mock.patch("subprocess.run", None)
|
|
@mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
|
|
@mock.patch("cephadm.services.nfs.NFSService.run_grace_tool", mock.MagicMock())
|
|
@mock.patch("cephadm.services.nfs.NFSService.create_rados_config_obj", mock.MagicMock())
|
|
@mock.patch("cephadm.services.nfs.NFSService.purge", mock.MagicMock())
|
|
- @mock.patch("subprocess.run", mock.MagicMock())
|
|
def test_apply_save(self, spec: ServiceSpec, meth, cephadm_module: CephadmOrchestrator):
|
|
with with_host(cephadm_module, 'test'):
|
|
with with_service(cephadm_module, spec, meth, 'test'):
|
|
--- a/src/pybind/mgr/dashboard/module.py
|
|
+++ b/src/pybind/mgr/dashboard/module.py
|
|
@@ -21,6 +21,7 @@ if TYPE_CHECKING:
|
|
else:
|
|
from typing_extensions import Literal
|
|
|
|
+from ceph.cryptotools.select import choose_crypto_caller
|
|
from mgr_module import CLIReadCommand, CLIWriteCommand, HandleCommandResult, \
|
|
MgrModule, MgrStandbyModule, NotifyType, Option, _get_localized_key
|
|
from mgr_util import ServerConfigException, build_url, \
|
|
@@ -340,7 +341,8 @@ class Module(MgrModule, CherryPyConfig):
|
|
min=400, max=599),
|
|
Option(name='redirect_resolve_ip_addr', type='bool', default=False),
|
|
Option(name='cross_origin_url', type='str', default=''),
|
|
- ]
|
|
+ Option(name='crypto_caller', type='str', default=''),
|
|
+]
|
|
MODULE_OPTIONS.extend(options_schema_list())
|
|
for options in PLUGIN_MANAGER.hook.get_options() or []:
|
|
MODULE_OPTIONS.extend(options)
|
|
@@ -353,6 +355,9 @@ class Module(MgrModule, CherryPyConfig):
|
|
def __init__(self, *args, **kwargs):
|
|
super(Module, self).__init__(*args, **kwargs)
|
|
CherryPyConfig.__init__(self)
|
|
+ # configure the dashboard's crypto caller. by default it will
|
|
+ # use the remote caller to avoid pyo3 conflicts
|
|
+ choose_crypto_caller(str(self.get_module_option('crypto_caller', '')))
|
|
|
|
mgr.init(self)
|
|
|
|
@@ -570,6 +575,9 @@ class StandbyModule(MgrStandbyModule, Ch
|
|
super(StandbyModule, self).__init__(*args, **kwargs)
|
|
CherryPyConfig.__init__(self)
|
|
self.shutdown_event = threading.Event()
|
|
+ # configure the dashboard's crypto caller. by default it will
|
|
+ # use the remote caller to avoid pyo3 conflicts
|
|
+ choose_crypto_caller(str(self.get_module_option('crypto_caller', '')))
|
|
|
|
# We can set the global mgr instance to ourselves even though
|
|
# we're just a standby, because it's enough for logging.
|
|
--- a/src/pybind/mgr/dashboard/services/access_control.py
|
|
+++ b/src/pybind/mgr/dashboard/services/access_control.py
|
|
@@ -12,7 +12,7 @@ from datetime import datetime, timedelta
|
|
from string import ascii_lowercase, ascii_uppercase, digits, punctuation
|
|
from typing import List, Optional, Sequence
|
|
|
|
-import bcrypt
|
|
+from ceph.cryptotools.select import get_crypto_caller
|
|
from mgr_module import CLICheckNonemptyFileInput, CLIReadCommand, CLIWriteCommand
|
|
from mgr_util import password_hash
|
|
|
|
@@ -888,7 +888,10 @@ def ac_user_set_password_hash(_, usernam
|
|
hashed_password = inbuf
|
|
try:
|
|
# make sure the hashed_password is actually a bcrypt hash
|
|
- bcrypt.checkpw(b'', hashed_password.encode('utf-8'))
|
|
+ # catch a ValueError if hashed_password is not valid.
|
|
+ cc = get_crypto_caller()
|
|
+ cc.verify_password('', hashed_password)
|
|
+
|
|
user = mgr.ACCESS_CTRL_DB.get_user(username)
|
|
user.set_password_hash(hashed_password)
|
|
|
|
--- a/src/pybind/mgr/mgr_util.py
|
|
+++ b/src/pybind/mgr/mgr_util.py
|
|
@@ -3,7 +3,6 @@ import os
|
|
if 'UNITTEST' in os.environ:
|
|
import tests
|
|
|
|
-import bcrypt
|
|
import cephfs
|
|
import contextlib
|
|
import datetime
|
|
@@ -25,6 +24,7 @@ else:
|
|
from typing import Tuple, Any, Callable, Optional, Dict, TYPE_CHECKING, TypeVar, List, Iterable, Generator, Generic, Iterator
|
|
|
|
from ceph.deployment.utils import wrap_ipv6
|
|
+from ceph.cryptotools.select import get_crypto_caller
|
|
|
|
T = TypeVar('T')
|
|
|
|
@@ -523,7 +523,7 @@ def create_self_signed_cert(organisation
|
|
|
|
:param organisation: String representing the Organisation(O) RDN (default='Ceph')
|
|
:param common_name: String representing the Common Name(CN) RDN (default='mgr')
|
|
- :param dname: Optional dictionary containing RDNs to use for crt/key generation
|
|
+ :param dname: Optional dictionary containing RDNs to use for crt/key generation
|
|
|
|
:return: ssl crt and key in utf-8 format
|
|
|
|
@@ -531,20 +531,9 @@ def create_self_signed_cert(organisation
|
|
|
|
"""
|
|
|
|
- from OpenSSL import crypto
|
|
- from uuid import uuid4
|
|
-
|
|
# RDN = Relative Distinguished Name
|
|
valid_RDN_list = ['C', 'ST', 'L', 'O', 'OU', 'CN', 'emailAddress']
|
|
|
|
- # create a key pair
|
|
- pkey = crypto.PKey()
|
|
- pkey.generate_key(crypto.TYPE_RSA, 2048)
|
|
-
|
|
- # Create a "subject" object
|
|
- req = crypto.X509Req()
|
|
- subj = req.get_subject()
|
|
-
|
|
if dname:
|
|
# dname received, so check it contains valid RDNs
|
|
if not all(field in valid_RDN_list for field in dname):
|
|
@@ -552,43 +541,18 @@ def create_self_signed_cert(organisation
|
|
else:
|
|
dname = {"O": organisation, "CN": common_name}
|
|
|
|
- # populate the subject with the dname settings
|
|
- for k, v in dname.items():
|
|
- setattr(subj, k, v)
|
|
-
|
|
- # create a self-signed cert
|
|
- cert = crypto.X509()
|
|
- cert.set_subject(req.get_subject())
|
|
- cert.set_serial_number(int(uuid4()))
|
|
- cert.gmtime_adj_notBefore(0)
|
|
- cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60) # 10 years
|
|
- cert.set_issuer(cert.get_subject())
|
|
- cert.set_pubkey(pkey)
|
|
- cert.sign(pkey, 'sha512')
|
|
-
|
|
- cert = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
|
|
- pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)
|
|
+ cc = get_crypto_caller()
|
|
+ pkey = cc.create_private_key()
|
|
+ cert = cc.create_self_signed_cert(dname, pkey)
|
|
+ return cert, pkey
|
|
|
|
- return cert.decode('utf-8'), pkey.decode('utf-8')
|
|
|
|
-
|
|
-def verify_cacrt_content(crt):
|
|
- # type: (str) -> None
|
|
- from OpenSSL import crypto
|
|
- try:
|
|
- crt_buffer = crt.encode("ascii") if isinstance(crt, str) else crt
|
|
- x509 = crypto.load_certificate(crypto.FILETYPE_PEM, crt_buffer)
|
|
- if x509.has_expired():
|
|
- org, cn = get_cert_issuer_info(crt)
|
|
- no_after = x509.get_notAfter()
|
|
- end_date = None
|
|
- if no_after is not None:
|
|
- end_date = datetime.datetime.strptime(no_after.decode('ascii'), '%Y%m%d%H%M%SZ')
|
|
- msg = f'Certificate issued by "{org}/{cn}" expired on {end_date}'
|
|
- logger.warning(msg)
|
|
- raise ServerConfigException(msg)
|
|
- except (ValueError, crypto.Error) as e:
|
|
- raise ServerConfigException(f'Invalid certificate: {e}')
|
|
+def certificate_days_to_expire(crt: str) -> int:
|
|
+ try:
|
|
+ cc = get_crypto_caller()
|
|
+ return cc.certificate_days_to_expire(crt)
|
|
+ except ValueError as err:
|
|
+ raise ServerConfigException(f'Invalid certificate: {err}')
|
|
|
|
|
|
def verify_cacrt(cert_fname):
|
|
@@ -602,58 +566,27 @@ def verify_cacrt(cert_fname):
|
|
|
|
try:
|
|
with open(cert_fname) as f:
|
|
- verify_cacrt_content(f.read())
|
|
+ certificate_days_to_expire(f.read())
|
|
except ValueError as e:
|
|
raise ServerConfigException(
|
|
'Invalid certificate {}: {}'.format(cert_fname, str(e)))
|
|
|
|
def get_cert_issuer_info(crt: str) -> Tuple[Optional[str],Optional[str]]:
|
|
"""Basic validation of a ca cert"""
|
|
-
|
|
- from OpenSSL import crypto, SSL
|
|
+ cc = get_crypto_caller()
|
|
try:
|
|
- crt_buffer = crt.encode("ascii") if isinstance(crt, str) else crt
|
|
- (org_name, cn) = (None, None)
|
|
- cert = crypto.load_certificate(crypto.FILETYPE_PEM, crt_buffer)
|
|
- components = cert.get_issuer().get_components()
|
|
- for c in components:
|
|
- if c[0].decode() == 'O': # org comp
|
|
- org_name = c[1].decode()
|
|
- elif c[0].decode() == 'CN': # common name comp
|
|
- cn = c[1].decode()
|
|
- return (org_name, cn)
|
|
- except (ValueError, crypto.Error) as e:
|
|
- raise ServerConfigException(f'Invalid certificate key: {e}')
|
|
+ return cc.get_cert_issuer_info(crt)
|
|
+ except ValueError as err:
|
|
+ raise ServerConfigException(f'Invalid certificate key: {err}')
|
|
+
|
|
|
|
def verify_tls(crt, key):
|
|
# type: (str, str) -> None
|
|
- verify_cacrt_content(crt)
|
|
-
|
|
- from OpenSSL import crypto, SSL
|
|
- try:
|
|
- _key = crypto.load_privatekey(crypto.FILETYPE_PEM, key)
|
|
- _key.check()
|
|
- except (ValueError, crypto.Error) as e:
|
|
- raise ServerConfigException(
|
|
- 'Invalid private key: {}'.format(str(e)))
|
|
+ cc = get_crypto_caller()
|
|
try:
|
|
- crt_buffer = crt.encode("ascii") if isinstance(crt, str) else crt
|
|
- _crt = crypto.load_certificate(crypto.FILETYPE_PEM, crt_buffer)
|
|
- except ValueError as e:
|
|
- raise ServerConfigException(
|
|
- 'Invalid certificate key: {}'.format(str(e))
|
|
- )
|
|
-
|
|
- try:
|
|
- context = SSL.Context(SSL.TLSv1_METHOD)
|
|
- context.use_certificate(_crt)
|
|
- context.use_privatekey(_key)
|
|
- context.check_privatekey()
|
|
- except crypto.Error as e:
|
|
- logger.warning('Private key and certificate do not match up: {}'.format(str(e)))
|
|
- except SSL.Error as e:
|
|
- raise ServerConfigException(f'Invalid cert/key pair: {e}')
|
|
-
|
|
+ cc.verify_tls(crt, key)
|
|
+ except ValueError as err:
|
|
+ raise ServerConfigException(str(err))
|
|
|
|
|
|
def verify_tls_files(cert_fname, pkey_fname):
|
|
@@ -681,24 +614,14 @@ def verify_tls_files(cert_fname, pkey_fn
|
|
if not os.path.isfile(pkey_fname):
|
|
raise ServerConfigException('private key %s does not exist' % pkey_fname)
|
|
|
|
- from OpenSSL import crypto, SSL
|
|
+ if not os.path.isfile(cert_fname):
|
|
+ raise ServerConfigException('certificate %s does not exist' % cert_fname)
|
|
|
|
try:
|
|
- with open(pkey_fname) as f:
|
|
- pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, f.read())
|
|
- pkey.check()
|
|
- except (ValueError, crypto.Error) as e:
|
|
- raise ServerConfigException(
|
|
- 'Invalid private key {}: {}'.format(pkey_fname, str(e)))
|
|
- try:
|
|
- context = SSL.Context(SSL.TLSv1_METHOD)
|
|
- context.use_certificate_file(cert_fname, crypto.FILETYPE_PEM)
|
|
- context.use_privatekey_file(pkey_fname, crypto.FILETYPE_PEM)
|
|
- context.check_privatekey()
|
|
- except crypto.Error as e:
|
|
- logger.warning(
|
|
- 'Private key {} and certificate {} do not match up: {}'.format(
|
|
- pkey_fname, cert_fname, str(e)))
|
|
+ with open(pkey_fname) as key_file, open(cert_fname) as cert_file:
|
|
+ verify_tls(cert_file.read(), key_file.read())
|
|
+ except (ServerConfigException) as e:
|
|
+ raise ServerConfigException({e})
|
|
|
|
|
|
def get_most_recent_rate(rates: Optional[List[Tuple[float, float]]]) -> float:
|
|
@@ -879,8 +802,9 @@ def profile_method(skip_attribute: bool
|
|
def password_hash(password: Optional[str], salt_password: Optional[str] = None) -> Optional[str]:
|
|
if not password:
|
|
return None
|
|
+
|
|
if not salt_password:
|
|
- salt = bcrypt.gensalt()
|
|
- else:
|
|
- salt = salt_password.encode('utf8')
|
|
- return bcrypt.hashpw(password.encode('utf8'), salt).decode('utf8')
|
|
+ salt_password = ''
|
|
+
|
|
+ cc = get_crypto_caller()
|
|
+ return cc.password_hash(password, salt_password)
|
|
--- a/src/pybind/mgr/tests/test_tls.py
|
|
+++ b/src/pybind/mgr/tests/test_tls.py
|
|
@@ -1,4 +1,4 @@
|
|
-from mgr_util import create_self_signed_cert, verify_tls, ServerConfigException, get_cert_issuer_info
|
|
+from mgr_util import create_self_signed_cert, verify_tls, ServerConfigException, get_cert_issuer_info, certificate_days_to_expire
|
|
from OpenSSL import crypto, SSL
|
|
|
|
import unittest
|
|
@@ -10,6 +10,9 @@ valid_ceph_cert = """-----BEGIN CERTIFIC
|
|
invalid_cert = """-----BEGIN CERTIFICATE-----\nMIICxjCCAa4CEQCpHIQuSYhCII1J0SVGYnT1MA0GCSqGSIb3DQEBDQUAMCExDTAL\nBgNVBAoMBENlcGgxEDAOBgNVBAMMB2NlcGhhZG0wHhcNMjIwNzA2MTE1MjUyWhcN\nMzIwNzAzMTE1MjUyWjAhMQ0wCwYDVQQKDARDZXBoMRAwDgYDVQQDDAdjZXBoYWRt\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEBn2ApFna2CVYE7RDtjJVk\ncJTcJQrjzDOlCoZtxb1QMCQZMXjx/7d6bseQP+dkkeA0hZxnjJZWeu6c/YnQ1JiT\n2aDuDpWoJAaiinHRJyZuY5tqG+ggn95RdToZVbeC+0uALzYi4UFacC3sfpkyIKBR\nic43+2fQNz0PZ+8INSTtm75Y53gbWuGF7Dv95200AmAN2/u8LKWZIvdhbRborxOF\nlK2T40qbj9eH3ewIN/6Eibxrvg4va3pIoOaq0XdJHAL/MjDGJAtahPIenwcjuega\n4PSlB0h3qiyFXz7BG8P0QsPP6slyD58ZJtCGtJiWPOhlq47DlnWlJzRGDEFLLryf\n8wIDAQABMA0GCSqGSIb3DQEBDQUAA4IBAQBixd7RZawlYiTZaCmv3Vy7X/hhabac\nE/YiuFt1YMe0C9+D8IcCQN/IRww/Bi7Af6tm+ncHT9GsOGWX6hahXDKTw3b9nSDi\nETvjkUTYOayZGfhYpRA6m6e/2ypcUYsiXRDY9zneDKCdPREIA1D6L2fROHetFX9r\nX9rSry01xrYwNlYA1e6GLMXm2NaGsLT3JJlRBtT3P7f1jtRGXcwkc7ns0AtW0uNj\nGqRLHfJazdgWJFsj8vBdMs7Ci0C/b5/f7J/DLpPCvUA3Fqwn9MzHl01UwlDsKy1a\nROi4cfQNOLbWX8g3PfIlqtdGYNA77UPxvy1SUimmtdopZa\n-----END CERTIFICATE-----\n
|
|
"""
|
|
|
|
+expired_cert = """-----BEGIN CERTIFICATE-----\nMIICtjCCAZ4CAQAwDQYJKoZIhvcNAQENBQAwITEQMA4GA1UEAwwHY2VwaGFkbTEN\nMAsGA1UECgwEQ2VwaDAeFw0xNTAyMTYxOTQ4MTdaFw0yMDAyMTUxOTQ4MTdaMCEx\nEDAOBgNVBAMMB2NlcGhhZG0xDTALBgNVBAoMBENlcGgwggEiMA0GCSqGSIb3DQEB\nAQUAA4IBDwAwggEKAoIBAQCxYHJ6RlPeuhZJyAMR1ru01BEGbwhI7vMga8pwyTX8\nNn1ow2asbj7lad+jO5j5Gon8GFwsrKM0S8vmITxd5QkshnHPQRQF8hz4aieNOQiL\nnVRBTHgLihEBJCpyuTmHLn1G374ZObuFqyHcnIrKNdeKb0JxNKbx26/2NrWwFGAe\nAj5KuizMHJMZYVLfYelP4g2hSRPe2JJWI4429LeLWuBQBL9t/IPY0IlmFDP4eL+S\nB2Py8Ig2XY5oyaaxpwI8H/cAY92lsoHPI3ldDn1JEiH5Gwzxf+9fF29cesp8BYcm\naav1jT8ONvsfn7AxKDKcfZIpRNKlOqFIC03gG5R3O1iHAgMBAAEwDQYJKoZIhvcN\nAQENBQADggEBADh9bAMR7RIK3M3u6LoTQQrcoxJ0pEXBrFQGQk2uz2krlDTKRS+2\nubwD8bLNd3dl5RzvVJ1hui8y9JMnqYwgMrjR9B0EDUM/ihYU2zO3ZN9nhhnTN2qT\n+UtFtyilg3U4nQdWGw2jFPu08JPoF/g+7iBH+/o5WOfzOovjLg4BsVlKUP4ND8Dv\nXr8gxZTlaoZvZlhMCdhiT2aKstCA9R3RYBbEo/FtcsHOkO0EFuxCLiVd/eo3F56C\njfVWnvqyz3r2f1G1VafvhhdlMJ4p35Hw1ms6nFTLx5dKwJW+Xve+qBU3Q5I5iV02\nAIXXBaqId/YqKXZd+Ge/XBmluXH929PtUOk=\n-----END CERTIFICATE-----\n
|
|
+"""
|
|
+
|
|
class TLSchecks(unittest.TestCase):
|
|
|
|
def test_defaults(self):
|
|
@@ -28,7 +31,7 @@ class TLSchecks(unittest.TestCase):
|
|
crt, key = create_self_signed_cert()
|
|
|
|
# fudge the key, to force an error to be detected during verify_tls
|
|
- fudged = f"{key[:-35]}c0ffee==\n{key[-25:]}".encode('utf-8')
|
|
+ fudged = f"{key[:-35]}c0ffee==\n{key[-25:]}"
|
|
self.assertRaises(ServerConfigException, verify_tls, crt, fudged)
|
|
|
|
def test_mismatched_tls(self):
|
|
@@ -38,7 +41,6 @@ class TLSchecks(unittest.TestCase):
|
|
new_key = crypto.PKey()
|
|
new_key.generate_key(crypto.TYPE_RSA, 2048)
|
|
new_key = crypto.dump_privatekey(crypto.FILETYPE_PEM, new_key).decode('utf-8')
|
|
-
|
|
self.assertRaises(ServerConfigException, verify_tls, crt, new_key)
|
|
|
|
def test_get_cert_issuer_info(self):
|
|
@@ -53,3 +55,8 @@ class TLSchecks(unittest.TestCase):
|
|
|
|
# invalid certificate
|
|
self.assertRaises(ServerConfigException, get_cert_issuer_info, invalid_cert)
|
|
+
|
|
+ # expired certificate
|
|
+ self.assertRaisesRegex(ServerConfigException,
|
|
+ 'Certificate issued by "Ceph/cephadm" expired',
|
|
+ certificate_days_to_expire, expired_cert)
|
|
--- /dev/null
|
|
+++ b/src/python-common/ceph/cryptotools/caller.py
|
|
@@ -0,0 +1,48 @@
|
|
+from typing import Dict, Tuple
|
|
+
|
|
+import abc
|
|
+
|
|
+
|
|
+class CryptoCallError(ValueError):
|
|
+ pass
|
|
+
|
|
+
|
|
+class CryptoCaller(abc.ABC):
|
|
+ """Abstract base class for `CryptoCaller`s - an interface that
|
|
+ encapsulates basic password and TLS cert related functions
|
|
+ needed by the Ceph MGR.
|
|
+ """
|
|
+
|
|
+ @abc.abstractmethod
|
|
+ def create_private_key(self) -> str:
|
|
+ """Create a new TLS private key, returning it as a string."""
|
|
+
|
|
+ @abc.abstractmethod
|
|
+ def create_self_signed_cert(
|
|
+ self, dname: Dict[str, str], pkey: str
|
|
+ ) -> str:
|
|
+ """Given TLS certificate subject parameters and a private key,
|
|
+ create a new self signed certificate - returned as a string.
|
|
+ """
|
|
+
|
|
+ @abc.abstractmethod
|
|
+ def verify_tls(self, crt: str, key: str) -> None:
|
|
+ """Given a TLS certificate and a private key raise an error
|
|
+ if the combination is not valid.
|
|
+ """
|
|
+
|
|
+ @abc.abstractmethod
|
|
+ def certificate_days_to_expire(self, crt: str) -> int:
|
|
+ """Return the number of days until the given TLS certificate expires."""
|
|
+
|
|
+ @abc.abstractmethod
|
|
+ def get_cert_issuer_info(self, crt: str) -> Tuple[str, str]:
|
|
+ """Basic validation of a ca cert"""
|
|
+
|
|
+ @abc.abstractmethod
|
|
+ def password_hash(self, password: str, salt_password: str) -> str:
|
|
+ """Hash a password. Returns the hashed password as a string."""
|
|
+
|
|
+ @abc.abstractmethod
|
|
+ def verify_password(self, password: str, hashed_password: str) -> bool:
|
|
+ """Return true if a password and hash match."""
|
|
--- /dev/null
|
|
+++ b/src/python-common/ceph/cryptotools/cryptotools.py
|
|
@@ -0,0 +1,135 @@
|
|
+"""
|
|
+This file has been isolated into a module so that it can be run
|
|
+in a subprocess therefore sidestepping the
|
|
+`PyO3 modules may only be initialized once per interpreter process` problem.
|
|
+"""
|
|
+
|
|
+from typing import Any, Dict
|
|
+
|
|
+import argparse
|
|
+import json
|
|
+import sys
|
|
+
|
|
+from argparse import Namespace
|
|
+
|
|
+from .internal import InternalCryptoCaller, InternalError
|
|
+
|
|
+
|
|
+def _read() -> str:
|
|
+ return sys.stdin.read()
|
|
+
|
|
+
|
|
+def _load() -> Dict[str, Any]:
|
|
+ return json.loads(_read())
|
|
+
|
|
+
|
|
+def _respond(data: Dict[str, Any]) -> None:
|
|
+ json.dump(data, sys.stdout)
|
|
+
|
|
+
|
|
+def _write(content: str) -> None:
|
|
+ sys.stdout.write(content)
|
|
+ sys.stdout.flush()
|
|
+
|
|
+
|
|
+def _fail(msg: str, code: int = 0) -> Any:
|
|
+ json.dump({'error': msg}, sys.stdout)
|
|
+ sys.exit(code)
|
|
+
|
|
+
|
|
+def password_hash(args: Namespace) -> None:
|
|
+ data = _load()
|
|
+ password = data['password']
|
|
+ salt_password = data['salt_password']
|
|
+ hash_str = args.crypto.password_hash(password, salt_password)
|
|
+ _respond({'hash': hash_str})
|
|
+
|
|
+
|
|
+def verify_password(args: Namespace) -> None:
|
|
+ data = _load()
|
|
+ password = data.get('password', '')
|
|
+ hashed_password = data.get('hashed_password', '')
|
|
+ try:
|
|
+ ok = args.crypto.verify_password(password, hashed_password)
|
|
+ except ValueError as err:
|
|
+ _fail(str(err))
|
|
+ _respond({'ok': ok})
|
|
+
|
|
+
|
|
+def create_private_key(args: Namespace) -> None:
|
|
+ _write(args.crypto.create_private_key())
|
|
+
|
|
+
|
|
+def create_self_signed_cert(args: Namespace) -> None:
|
|
+ data = _load()
|
|
+ dname = data['dname']
|
|
+ private_key = data['private_key']
|
|
+ _write(args.crypto.create_self_signed_cert(dname, private_key))
|
|
+
|
|
+
|
|
+def certificate_days_to_expire(args: Namespace) -> None:
|
|
+ crt = _read()
|
|
+ try:
|
|
+ days_until_exp = args.crypto.certificate_days_to_expire(crt)
|
|
+ except InternalError as err:
|
|
+ _fail(str(err))
|
|
+ _respond({'days_until_expiration': days_until_exp})
|
|
+
|
|
+
|
|
+def get_cert_issuer_info(args: Namespace) -> None:
|
|
+ crt = _read()
|
|
+ org_name, cn = args.crypto.get_cert_issuer_info(crt)
|
|
+ _respond({'org_name': org_name, 'cn': cn})
|
|
+
|
|
+
|
|
+def verify_tls(args: Namespace) -> None:
|
|
+ data = _load()
|
|
+ crt = data['crt']
|
|
+ key = data['key']
|
|
+ try:
|
|
+ args.crypto.verify_tls(crt, key)
|
|
+ except ValueError as err:
|
|
+ _fail(str(err))
|
|
+ _respond({'ok': True}) # need to emit something on success
|
|
+
|
|
+
|
|
+def main() -> None:
|
|
+ # create the top-level parser
|
|
+ parser = argparse.ArgumentParser(prog='cryptotools.py')
|
|
+ parser.set_defaults(crypto=InternalCryptoCaller())
|
|
+ subparsers = parser.add_subparsers(required=True)
|
|
+
|
|
+ # create the parser for the "password_hash" command
|
|
+ parser_password_hash = subparsers.add_parser('password_hash')
|
|
+ parser_password_hash.set_defaults(func=password_hash)
|
|
+
|
|
+ # create the parser for the "create_self_signed_cert" command
|
|
+ parser_cssc = subparsers.add_parser('create_self_signed_cert')
|
|
+ parser_cssc.set_defaults(func=create_self_signed_cert)
|
|
+
|
|
+ parser_cpk = subparsers.add_parser('create_private_key')
|
|
+ parser_cpk.set_defaults(func=create_private_key)
|
|
+
|
|
+ # create the parser for the "certificate_days_to_expire" command
|
|
+ parser_dte = subparsers.add_parser('certificate_days_to_expire')
|
|
+ parser_dte.set_defaults(func=certificate_days_to_expire)
|
|
+
|
|
+ # create the parser for the "get_cert_issuer_info" command
|
|
+ parser_gcii = subparsers.add_parser('get_cert_issuer_info')
|
|
+ parser_gcii.set_defaults(func=get_cert_issuer_info)
|
|
+
|
|
+ # create the parser for the "verify_tls" command
|
|
+ parser_verify_tls = subparsers.add_parser('verify_tls')
|
|
+ parser_verify_tls.set_defaults(func=verify_tls)
|
|
+
|
|
+ # password verification
|
|
+ parser_verify_password = subparsers.add_parser('verify_password')
|
|
+ parser_verify_password.set_defaults(func=verify_password)
|
|
+
|
|
+ # parse the args and call whatever function was selected
|
|
+ args = parser.parse_args()
|
|
+ args.func(args)
|
|
+
|
|
+
|
|
+if __name__ == "__main__":
|
|
+ main()
|
|
--- /dev/null
|
|
+++ b/src/python-common/ceph/cryptotools/internal.py
|
|
@@ -0,0 +1,134 @@
|
|
+"""Internal execution of cryptographic functions for the ceph mgr
|
|
+"""
|
|
+
|
|
+from typing import Dict, Any, Tuple, Union
|
|
+
|
|
+from uuid import uuid4
|
|
+import datetime
|
|
+import warnings
|
|
+
|
|
+from OpenSSL import crypto, SSL
|
|
+import bcrypt
|
|
+
|
|
+
|
|
+from .caller import CryptoCaller, CryptoCallError
|
|
+
|
|
+
|
|
+class InternalError(CryptoCallError):
|
|
+ pass
|
|
+
|
|
+
|
|
+class InternalCryptoCaller(CryptoCaller):
|
|
+ def fail(self, msg: str) -> None:
|
|
+ raise InternalError(msg)
|
|
+
|
|
+ def password_hash(self, password: str, salt_password: str) -> str:
|
|
+ salt = salt_password.encode() if salt_password else bcrypt.gensalt()
|
|
+ return bcrypt.hashpw(password.encode(), salt).decode()
|
|
+
|
|
+ def verify_password(self, password: str, hashed_password: str) -> bool:
|
|
+ _password = password.encode()
|
|
+ _hashed_password = hashed_password.encode()
|
|
+ try:
|
|
+ ok = bcrypt.checkpw(_password, _hashed_password)
|
|
+ except ValueError as err:
|
|
+ self.fail(str(err))
|
|
+ return ok
|
|
+
|
|
+ def create_private_key(self) -> str:
|
|
+ pkey = crypto.PKey()
|
|
+ pkey.generate_key(crypto.TYPE_RSA, 2048)
|
|
+ return crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey).decode()
|
|
+
|
|
+ def create_self_signed_cert(
|
|
+ self, dname: Dict[str, str], pkey: str
|
|
+ ) -> str:
|
|
+ _pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, pkey)
|
|
+
|
|
+ # Create a "subject" object
|
|
+ with warnings.catch_warnings():
|
|
+ warnings.simplefilter("ignore")
|
|
+ req = crypto.X509Req()
|
|
+ subj = req.get_subject()
|
|
+
|
|
+ # populate the subject with the dname settings
|
|
+ for k, v in dname.items():
|
|
+ setattr(subj, k, v)
|
|
+
|
|
+ # create a self-signed cert
|
|
+ cert = crypto.X509()
|
|
+ cert.set_subject(req.get_subject())
|
|
+ cert.set_serial_number(int(uuid4()))
|
|
+ cert.gmtime_adj_notBefore(0)
|
|
+ cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60) # 10 years
|
|
+ cert.set_issuer(cert.get_subject())
|
|
+ cert.set_pubkey(_pkey)
|
|
+ cert.sign(_pkey, 'sha512')
|
|
+ return crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode()
|
|
+
|
|
+ def _load_cert(self, crt: Union[str, bytes]) -> Any:
|
|
+ crt_buffer = crt.encode() if isinstance(crt, str) else crt
|
|
+ try:
|
|
+ cert = crypto.load_certificate(crypto.FILETYPE_PEM, crt_buffer)
|
|
+ except (ValueError, crypto.Error) as e:
|
|
+ self.fail('Invalid certificate: %s' % str(e))
|
|
+ return cert
|
|
+
|
|
+ def _issuer_info(self, cert: Any) -> Tuple[str, str]:
|
|
+ components = cert.get_issuer().get_components()
|
|
+ org_name = cn = ''
|
|
+ for c in components:
|
|
+ if c[0].decode() == 'O': # org comp
|
|
+ org_name = c[1].decode()
|
|
+ elif c[0].decode() == 'CN': # common name comp
|
|
+ cn = c[1].decode()
|
|
+ return (org_name, cn)
|
|
+
|
|
+ def certificate_days_to_expire(self, crt: str) -> int:
|
|
+ x509 = self._load_cert(crt)
|
|
+ no_after = x509.get_notAfter()
|
|
+ if not no_after:
|
|
+ self.fail("Certificate does not have an expiration date.")
|
|
+
|
|
+ end_date = datetime.datetime.strptime(
|
|
+ no_after.decode(), '%Y%m%d%H%M%SZ'
|
|
+ )
|
|
+
|
|
+ if x509.has_expired():
|
|
+ org, cn = self._issuer_info(x509)
|
|
+ msg = 'Certificate issued by "%s/%s" expired on %s' % (
|
|
+ org,
|
|
+ cn,
|
|
+ end_date,
|
|
+ )
|
|
+ self.fail(msg)
|
|
+
|
|
+ # Certificate still valid, calculate and return days until expiration
|
|
+ with warnings.catch_warnings():
|
|
+ warnings.simplefilter("ignore")
|
|
+ days_until_exp = (end_date - datetime.datetime.utcnow()).days
|
|
+ return int(days_until_exp)
|
|
+
|
|
+ def get_cert_issuer_info(self, crt: str) -> Tuple[str, str]:
|
|
+ return self._issuer_info(self._load_cert(crt))
|
|
+
|
|
+ def verify_tls(self, crt: str, key: str) -> None:
|
|
+ try:
|
|
+ _key = crypto.load_privatekey(crypto.FILETYPE_PEM, key)
|
|
+ _key.check()
|
|
+ except (ValueError, crypto.Error) as e:
|
|
+ self.fail('Invalid private key: %s' % str(e))
|
|
+ _crt = self._load_cert(crt)
|
|
+ try:
|
|
+ context = SSL.Context(SSL.TLSv1_METHOD)
|
|
+ with warnings.catch_warnings():
|
|
+ warnings.simplefilter("ignore")
|
|
+ context.use_certificate(_crt)
|
|
+ context.use_privatekey(_key)
|
|
+ context.check_privatekey()
|
|
+ except crypto.Error as e:
|
|
+ self.fail(
|
|
+ 'Private key and certificate do not match up: %s' % str(e)
|
|
+ )
|
|
+ except SSL.Error as e:
|
|
+ self.fail(f'Invalid cert/key pair: {e}')
|
|
--- /dev/null
|
|
+++ b/src/python-common/ceph/cryptotools/remote.py
|
|
@@ -0,0 +1,183 @@
|
|
+"""Remote execution of cryptographic functions for the ceph mgr
|
|
+"""
|
|
+
|
|
+# NB. This module exists to enapsulate the logic around running
|
|
+# the cryptotools module that are forked off of the parent process
|
|
+# to avoid the pyo3 subintepreters problem.
|
|
+#
|
|
+# The current implementation is simple using the command line and either raw
|
|
+# blobs or JSON as stdin inputs and raw blobs or JSON as outputs. It is important
|
|
+# that we avoid putting the sensitive data on the command line as that
|
|
+# is visible in /proc.
|
|
+#
|
|
+# This simple implementation incurs the cost of starting a python process
|
|
+# for every function call. CryptoCaller is written as a class so that if
|
|
+# we choose to we can have multiple implementations of the CryptoCaller
|
|
+# sharing the same protocol.
|
|
+# For instance we could have a persistent process listening on a unix
|
|
+# socket accepting the crypto functions as RPCs. For now, we keep it
|
|
+# simple though :-)
|
|
+
|
|
+from typing import List, Union, Dict, Any, Optional, Tuple
|
|
+
|
|
+import json
|
|
+import logging
|
|
+import subprocess
|
|
+
|
|
+from .caller import CryptoCaller, CryptoCallError
|
|
+
|
|
+
|
|
+_ctmodule = 'ceph.cryptotools.cryptotools'
|
|
+
|
|
+logger = logging.getLogger('ceph.cryptotools.remote')
|
|
+
|
|
+
|
|
+class ProcessCryptoCaller(CryptoCaller):
|
|
+ """ProcessCryptoCaller encapsulates cryptographic functions used by the
|
|
+ ceph mgr into a suite of functions that can be executed in a
|
|
+ different process.
|
|
+ Running the crypto functions in a separate process avoids conflicts
|
|
+ between the mgr's use of subintepreters and the cryptography module's
|
|
+ use of PyO3 rust bindings.
|
|
+
|
|
+ If you want to raise different error types set the json_error_cls
|
|
+ attribute and/or subclass and override the map_error method.
|
|
+ """
|
|
+
|
|
+ def __init__(
|
|
+ self, errors_from_json: bool = True, module: str = _ctmodule
|
|
+ ):
|
|
+ self._module = module
|
|
+ self.errors_from_json = errors_from_json
|
|
+ self.json_error_cls = ValueError
|
|
+
|
|
+ def _run(
|
|
+ self,
|
|
+ args: List[str],
|
|
+ input_data: Union[str, None] = None,
|
|
+ capture_output: bool = False,
|
|
+ check: bool = False,
|
|
+ ) -> subprocess.CompletedProcess:
|
|
+ if input_data is None:
|
|
+ _input = None
|
|
+ else:
|
|
+ _input = input_data.encode()
|
|
+ cmd = ['python3', '-m', _ctmodule] + list(args)
|
|
+ logger.warning('CryptoCaller will run: %r', cmd)
|
|
+ try:
|
|
+ return subprocess.run(
|
|
+ cmd, capture_output=capture_output, input=_input, check=check
|
|
+ )
|
|
+ except Exception as err:
|
|
+ mapped_err = self.map_error(err)
|
|
+ if mapped_err:
|
|
+ raise mapped_err from err
|
|
+ raise
|
|
+
|
|
+ def _result_json(self, result: subprocess.CompletedProcess) -> Any:
|
|
+ result_obj = json.loads(result.stdout)
|
|
+ if self.errors_from_json and 'error' in result_obj:
|
|
+ raise self.json_error_cls(str(result_obj['error']))
|
|
+ return result_obj
|
|
+
|
|
+ def _result_str(self, result: subprocess.CompletedProcess) -> str:
|
|
+ return result.stdout.decode()
|
|
+
|
|
+ def map_error(self, err: Exception) -> Optional[Exception]:
|
|
+ """Convert between error types raised by the subprocesses
|
|
+ running the crypto functions and what the mgr caller expects.
|
|
+ """
|
|
+ if isinstance(err, subprocess.CalledProcessError):
|
|
+ return CryptoCallError(
|
|
+ f'failed crypto call: {err.cmd}: {err.stderr}'
|
|
+ )
|
|
+ return None
|
|
+
|
|
+ def create_private_key(self) -> str:
|
|
+ """Create a new TLS private key, returning it as a string."""
|
|
+ result = self._run(
|
|
+ ['create_private_key'],
|
|
+ capture_output=True,
|
|
+ check=True,
|
|
+ )
|
|
+ return self._result_str(result).strip()
|
|
+
|
|
+ def create_self_signed_cert(
|
|
+ self, dname: Dict[str, str], pkey: str
|
|
+ ) -> str:
|
|
+ """Given TLS certificate subject parameters and a private key,
|
|
+ create a new self signed certificate - returned as a string.
|
|
+ """
|
|
+ result = self._run(
|
|
+ ['create_self_signed_cert'],
|
|
+ input_data=json.dumps({'dname': dname, 'private_key': pkey}),
|
|
+ capture_output=True,
|
|
+ check=True,
|
|
+ )
|
|
+ return self._result_str(result).strip()
|
|
+
|
|
+ def verify_tls(self, crt: str, key: str) -> None:
|
|
+ """Given a TLS certificate and a private key raise an error
|
|
+ if the combination is not valid.
|
|
+ """
|
|
+ result = self._run(
|
|
+ ['verify_tls'],
|
|
+ input_data=json.dumps({'crt': crt, 'key': key}),
|
|
+ capture_output=True,
|
|
+ check=True,
|
|
+ )
|
|
+ self._result_json(result) # for errors only
|
|
+
|
|
+ def certificate_days_to_expire(self, crt: str) -> int:
|
|
+ """Verify a CA Certificate return the number of days until expiration."""
|
|
+ result = self._run(
|
|
+ ["certificate_days_to_expire"],
|
|
+ input_data=crt,
|
|
+ capture_output=True,
|
|
+ check=True,
|
|
+ )
|
|
+ result_obj = self._result_json(result)
|
|
+ return int(result_obj['days_until_expiration'])
|
|
+
|
|
+ def get_cert_issuer_info(self, crt: str) -> Tuple[str, str]:
|
|
+ """Basic validation of a ca cert"""
|
|
+ result = self._run(
|
|
+ ["get_cert_issuer_info"],
|
|
+ input_data=crt,
|
|
+ capture_output=True,
|
|
+ check=True,
|
|
+ )
|
|
+ result_obj = self._result_json(result)
|
|
+ org_name = str(result_obj.get('org_name', ''))
|
|
+ cn = str(result_obj.get('cn', ''))
|
|
+ return org_name, cn
|
|
+
|
|
+ def password_hash(self, password: str, salt_password: str) -> str:
|
|
+ """Hash a password. Returns the hashed password as a string."""
|
|
+ pwdata = {"password": password, "salt_password": salt_password}
|
|
+ result = self._run(
|
|
+ ["password_hash"],
|
|
+ input_data=json.dumps(pwdata),
|
|
+ capture_output=True,
|
|
+ check=True,
|
|
+ )
|
|
+ result_obj = self._result_json(result)
|
|
+ pw_hash = result_obj.get("hash")
|
|
+ if not pw_hash:
|
|
+ raise CryptoCallError('no password hash')
|
|
+ return pw_hash
|
|
+
|
|
+ def verify_password(self, password: str, hashed_password: str) -> bool:
|
|
+ """Verify a password matches the hashed password. Returns true if
|
|
+ password and hashed_password match.
|
|
+ """
|
|
+ pwdata = {"password": password, "hashed_password": hashed_password}
|
|
+ result = self._run(
|
|
+ ["verify_password"],
|
|
+ input_data=json.dumps(pwdata),
|
|
+ capture_output=True,
|
|
+ check=True,
|
|
+ )
|
|
+ result_obj = self._result_json(result)
|
|
+ ok = result_obj.get("ok", False)
|
|
+ return ok
|
|
--- /dev/null
|
|
+++ b/src/python-common/ceph/cryptotools/select.py
|
|
@@ -0,0 +1,51 @@
|
|
+from typing import Dict
|
|
+
|
|
+import os
|
|
+
|
|
+from .caller import CryptoCaller
|
|
+
|
|
+
|
|
+_CC_ENV = 'CEPH_CRYPTOCALER'
|
|
+_CC_KEY = 'crypto_caller'
|
|
+_CC_REMOTE = 'remote'
|
|
+_CC_INTERNAL = 'internal'
|
|
+
|
|
+_CACHE: Dict[str, CryptoCaller] = {}
|
|
+
|
|
+
|
|
+def _check_name(name: str) -> None:
|
|
+ if name and name not in (_CC_REMOTE, _CC_INTERNAL):
|
|
+ raise ValueError(f'unexpected crypto caller name: {name}')
|
|
+
|
|
+
|
|
+def choose_crypto_caller(name: str = '') -> None:
|
|
+ _check_name(name)
|
|
+ if not name:
|
|
+ name = os.environ.get(_CC_ENV, '')
|
|
+ _check_name(name)
|
|
+ if not name:
|
|
+ name = _CC_REMOTE
|
|
+
|
|
+ if name == _CC_REMOTE:
|
|
+ import ceph.cryptotools.remote
|
|
+
|
|
+ _CACHE[_CC_KEY] = ceph.cryptotools.remote.ProcessCryptoCaller()
|
|
+ return
|
|
+ if name == _CC_INTERNAL:
|
|
+ import ceph.cryptotools.internal
|
|
+
|
|
+ _CACHE[_CC_KEY] = ceph.cryptotools.internal.InternalCryptoCaller()
|
|
+ return
|
|
+ # should be unreachable
|
|
+ raise RuntimeError('failed to setup a valid crypto caller')
|
|
+
|
|
+
|
|
+def get_crypto_caller() -> CryptoCaller:
|
|
+ """Return the currently selected crypto caller object."""
|
|
+ caller = _CACHE.get(_CC_KEY)
|
|
+ if not caller:
|
|
+ choose_crypto_caller()
|
|
+ caller = _CACHE.get(_CC_KEY)
|
|
+ if caller is None:
|
|
+ raise RuntimeError('failed to select crypto caller')
|
|
+ return caller
|
|
--- /dev/null
|
|
+++ b/src/python-common/ceph/cryptotools/__init__.py
|
|
@@ -0,0 +1 @@
|
|
+
|
|
--- a/src/pybind/mgr/restful/module.py
|
|
+++ b/src/pybind/mgr/restful/module.py
|
|
@@ -19,11 +19,11 @@ from . import context
|
|
|
|
from uuid import uuid4
|
|
from pecan import jsonify, make_app
|
|
-from OpenSSL import crypto
|
|
from pecan.rest import RestController
|
|
from werkzeug.serving import make_server, make_ssl_devcert
|
|
|
|
from .hooks import ErrorHook
|
|
+from ceph.cryptotools.select import get_crypto_caller
|
|
from mgr_module import MgrModule, CommandResult, NotifyType, Option
|
|
from mgr_util import build_url
|
|
|
|
@@ -402,25 +402,10 @@ class Module(MgrModule):
|
|
|
|
def create_self_signed_cert(self):
|
|
# create a key pair
|
|
- pkey = crypto.PKey()
|
|
- pkey.generate_key(crypto.TYPE_RSA, 2048)
|
|
-
|
|
- # create a self-signed cert
|
|
- cert = crypto.X509()
|
|
- cert.get_subject().O = "IT"
|
|
- cert.get_subject().CN = "ceph-restful"
|
|
- cert.set_serial_number(int(uuid4()))
|
|
- cert.gmtime_adj_notBefore(0)
|
|
- cert.gmtime_adj_notAfter(10*365*24*60*60)
|
|
- cert.set_issuer(cert.get_subject())
|
|
- cert.set_pubkey(pkey)
|
|
- cert.sign(pkey, 'sha512')
|
|
-
|
|
- return (
|
|
- crypto.dump_certificate(crypto.FILETYPE_PEM, cert),
|
|
- crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)
|
|
- )
|
|
-
|
|
+ cc = get_crypto_caller()
|
|
+ pkey = cc.create_private_key()
|
|
+ cert = cc.create_self_signed_cert({"O": "IT", "CN": "ceph-restful"}, pkey)
|
|
+ return cert, pkey
|
|
|
|
def handle_command(self, inbuf, command):
|
|
self.log.warning("Handling command: '%s'" % str(command))
|