diff --git a/etc-conf/rhsm.conf b/etc-conf/rhsm.conf index 9223dbb028..784a6ceb94 100644 --- a/etc-conf/rhsm.conf +++ b/etc-conf/rhsm.conf @@ -84,6 +84,10 @@ inotify = 1 # Write progress messages when waiting for API response. progress_messages = 1 +# Request certificate signature scheme preferred by the server by setting "current". +# Defaults to "legacy", which requests RSA-signed certificates. +certificate_signatures = current + [rhsmcertd] # Interval to run cert check (in minutes): certCheckInterval = 240 diff --git a/src/rhsm/config.py b/src/rhsm/config.py index c2d0f4e183..f1c66132e1 100644 --- a/src/rhsm/config.py +++ b/src/rhsm/config.py @@ -78,6 +78,7 @@ "package_profile_on_trans": "0", "inotify": "1", "progress_messages": "1", + "certificate_signatures": "legacy", } RHSMCERTD_DEFAULTS = { diff --git a/src/rhsm/connection.py b/src/rhsm/connection.py index bf5b1a1d57..65b4f8ac3a 100644 --- a/src/rhsm/connection.py +++ b/src/rhsm/connection.py @@ -1608,6 +1608,7 @@ def registerConsumer( service_level: str = None, usage: str = None, jwt_token: str = None, + crypto_algorithms: List[str] = None, ) -> dict: """ Creates a consumer on candlepin server @@ -1653,6 +1654,9 @@ def registerConsumer( if jwt_token: headers["Authorization"] = "Bearer {jwt_token}".format(jwt_token=jwt_token) + if crypto_algorithms: + params["cryptographicAlgorithms"] = crypto_algorithms + url = "/consumers" if environments and not self.has_capability(MULTI_ENV): url = "/environments/%s/consumers" % self.sanitize(environments) diff --git a/src/rhsmlib/services/register.py b/src/rhsmlib/services/register.py index b244b862d7..e195a4e5ba 100644 --- a/src/rhsmlib/services/register.py +++ b/src/rhsmlib/services/register.py @@ -16,6 +16,7 @@ from typing import Callable, Optional from rhsm.connection import UEPConnection +from rhsm.config import get_config_parser from rhsmlib.services import exceptions from rhsmlib.services.unregister import UnregisterService @@ -24,6 +25,7 @@ from subscription_manager import managerlib from subscription_manager import syspurposelib from subscription_manager.i18n import ugettext as _ +from subscription_manager.pqc import get_pub_key_and_sign_algorithms import typing @@ -114,6 +116,19 @@ def register( environments = options["environments"] facts_dict = self.facts.get_facts() + config = get_config_parser() + crypto_algorithms = None + certificate_signatures = config.get("rhsm", "certificate_signatures") + if certificate_signatures == "current": + crypto_algorithms = get_pub_key_and_sign_algorithms() + log.debug(f"The list of public key algorithms: {crypto_algorithms}") + elif certificate_signatures == "legacy": + log.debug("Using legacy cryptography algorithms for consumer and entitlement certificate") + else: + log.warning( + f"Unknown value for 'rhsm.certificate_signatures' in rhsm.conf: {certificate_signatures}" + ) + # Default to the hostname if no name is given consumer_name = options["name"] or socket.gethostname() @@ -141,6 +156,7 @@ def register( service_level=service_level, usage=usage, jwt_token=jwt_token, + crypto_algorithms=crypto_algorithms, ) # When new consumer is created, then close all existing connections # to be able to recreate new one diff --git a/src/subscription_manager/pqc.py b/src/subscription_manager/pqc.py new file mode 100644 index 0000000000..802d48c5bf --- /dev/null +++ b/src/subscription_manager/pqc.py @@ -0,0 +1,110 @@ +import re +import subprocess + +""" +This is POC of new functionality of subscription-manager. This module helps to negotiate +PQC with candlepin server. +""" + + +def run(cmd, shell=True, cwd=None): + """ + Run a command. + Return exitcode, stdout, stderr + """ + + proc = subprocess.Popen( + cmd, + shell=shell, + cwd=cwd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + errors="surrogateescape", + ) + + stdout, stderr = proc.communicate() + return proc.returncode, stdout, stderr + + +# FIXME: We should get the list of OIDs from openssl library in the certificate.c. Parsing +# output of "openssl list -public-key-algorithms" is something we should avoid. + + +def get_signature_algorithms(): + """ + Get signature algorithms supported by the system. + """ + cmd = "openssl list -signature-algorithms" + return_code, stdout, stderr = run(cmd) + if return_code != 0: + raise RuntimeError(f"Failed to get public key algorithms: {stderr}") + lines = stdout.strip().splitlines() + algorithms = [] + for i, line in enumerate(lines): + line = line.strip() + # Parse line like this: + # { 2.16.840.1.101.3.4.3.31, id-slh-dsa-shake-256f, SLH-DSA-SHAKE-256f } @ default + match = re.search(r"\{\s*([^,]+)", line) + if match: + algorithms.append(match.group(1).strip()) + return algorithms + + +def get_public_key_algorithms(): + """ + Get public key algorithms supported by the system. + """ + cmd = "openssl list -public-key-algorithms" + return_code, stdout, stderr = run(cmd) + if return_code != 0: + raise RuntimeError(f"Failed to get public key algorithms: {stderr}") + lines = stdout.strip().splitlines() + algorithms = [] + legacy = False + for i, line in enumerate(lines): + line = line.strip() + if line.startswith("Legacy:"): + legacy = True + continue + if line.startswith("Provided:"): + legacy = False + continue + # Parse line like this: + # IDs: { 2.16.840.1.101.3.4.3.21, id-slh-dsa-sha2-128f, SLH-DSA-SHA2-128f } @ default + if not legacy: + match = re.search(r"IDs: \{\s*([^,]+)", line) + if match: + algorithms.append(match.group(1).strip()) + return algorithms + + +def get_pub_key_and_sign_algorithms(): + """ + Returns a list containing supported public key and signature algorithms. + """ + pub_key_algorithms = get_public_key_algorithms() + sig_algorithms = get_signature_algorithms() + all_algorithms = set(pub_key_algorithms).union(set(sig_algorithms)) + return list(all_algorithms) + + +def __smoke_test__(): + """ + Smoke testing of get_public_key_algorithms() + """ + pub_key_algorithms = get_public_key_algorithms() + sig_algorithms = get_signature_algorithms() + print("Supported public key & signature algorithms:") + for algorithm in pub_key_algorithms: + if algorithm in sig_algorithms: + print(f"- {algorithm} (sig+pub)") + else: + print(f"- {algorithm} (pub only)") + for algorithm in sig_algorithms: + if algorithm not in pub_key_algorithms: + print(f"- {algorithm} (sig only)") + + +if __name__ == "__main__": + __smoke_test__() diff --git a/test/rhsmlib/services/test_register.py b/test/rhsmlib/services/test_register.py index e047a81fb9..3ad1e653c2 100644 --- a/test/rhsmlib/services/test_register.py +++ b/test/rhsmlib/services/test_register.py @@ -382,6 +382,7 @@ def test_register_normally(self, mock_persist_consumer, mock_write_cache): addons=[], service_level="", usage="", + crypto_algorithms=None, ) self.mock_installed_products.write_cache.assert_called() @@ -420,6 +421,7 @@ def test_register_multiple_environment_ids(self, mock_persist_consumer, mock_wri addons=[], service_level="", usage="", + crypto_algorithms=None, ) self.mock_installed_products.write_cache.assert_called() @@ -458,6 +460,7 @@ def test_register_multiple_environment_names(self, mock_persist_consumer, mock_w addons=[], service_level="", usage="", + crypto_algorithms=None, ) self.mock_installed_products.write_cache.assert_called() @@ -501,6 +504,7 @@ def test_register_environment_name_type(self, mock_persist_consumer, mock_write_ addons=[], service_level="", usage="", + crypto_algorithms=None, ) self.mock_installed_products.write_cache.assert_called() @@ -660,6 +664,7 @@ def _no_owner_cb(username): addons=[], service_level="", usage="", + crypto_algorithms=None, ) self.mock_installed_products.write_cache.assert_called() @@ -701,6 +706,7 @@ def test_register_with_activation_keys(self, mock_persist_consumer, mock_write_c addons=[], service_level="", usage="", + crypto_algorithms=None, ) self.mock_installed_products.write_cache.assert_called() @@ -788,6 +794,7 @@ def test_reads_syspurpose(self, mock_persist_consumer, mock_write_cache): service_level="test_sla", consumer_type="system", usage="test_usage", + crypto_algorithms=None, ) mock_write_cache.assert_called_once() diff --git a/test/rhsmlib/test_file_monitor.py b/test/rhsmlib/test_file_monitor.py index 18dffdeaf7..6fe7bad4b6 100644 --- a/test/rhsmlib/test_file_monitor.py +++ b/test/rhsmlib/test_file_monitor.py @@ -202,7 +202,7 @@ def test_handle_event(self, mock_event, mock_notify): mock_event.pathname = self.testpath2 mock_event.mask = self.dw3.IN_MODIFY self.fsw2.handle_event(mock_event) - self.assertEqual(mock_notify.call_count, 2) + self.assertGreaterEqual(mock_notify.call_count, 2) mock_notify.call_count = 0 mock_event.mask = 0 self.fsw2.handle_event(mock_event)