Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions etc-conf/rhsm.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/rhsm/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"package_profile_on_trans": "0",
"inotify": "1",
"progress_messages": "1",
"certificate_signatures": "legacy",
}

RHSMCERTD_DEFAULTS = {
Expand Down
4 changes: 4 additions & 0 deletions src/rhsm/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
16 changes: 16 additions & 0 deletions src/rhsmlib/services/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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
Expand Down
110 changes: 110 additions & 0 deletions src/subscription_manager/pqc.py
Original file line number Diff line number Diff line change
@@ -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",
)
Comment on lines +16 to +24
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.lang.security.audit.dangerous-subprocess-use-audit): Detected subprocess function 'Popen' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

Source: opengrep


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__()
7 changes: 7 additions & 0 deletions test/rhsmlib/services/test_register.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -660,6 +664,7 @@ def _no_owner_cb(username):
addons=[],
service_level="",
usage="",
crypto_algorithms=None,
)
self.mock_installed_products.write_cache.assert_called()

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand Down
2 changes: 1 addition & 1 deletion test/rhsmlib/test_file_monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading