| Current Path : /snap/lxd/current/lib/python3/dist-packages/ceph/cryptotools/ |
| Current File : //snap/lxd/current/lib/python3/dist-packages/ceph/cryptotools/remote.py |
"""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