From 7b0e9ef4ef606c3b9b4d5e02adb8ee7bd5f5fc4e Mon Sep 17 00:00:00 2001 From: florianhandke Date: Tue, 6 Oct 2026 11:28:41 +0200 Subject: [PATCH 1/2] Refactor AutoGenPKI implementation and update related forms, views, and templates --- trustpoint/management/forms.py | 95 +++-------- .../tests/test_forms/test_security_config.py | 83 +--------- .../test_views/test_backend_configuration.py | 2 +- .../tests/test_views/test_settings.py | 31 +--- .../management/views/backend_configuration.py | 4 +- trustpoint/management/views/settings.py | 13 +- trustpoint/pki/auto_gen_pki.py | 89 +++++----- trustpoint/pki/forms/__init__.py | 2 + trustpoint/pki/forms/issuing_cas.py | 28 ++++ trustpoint/pki/tests/test_auto_gen_pki.py | 24 ++- .../pki/tests/test_auto_gen_pki_lifecycle.py | 6 +- .../pki/tests/test_util_keys_extended.py | 63 -------- .../tests/test_views_issuing_cas_autogen.py | 152 ++++++++++++++++++ trustpoint/pki/urls.py | 5 + trustpoint/pki/util/keys.py | 37 ----- trustpoint/pki/views/issuing_cas.py | 71 ++++++++ .../management/backend_configuration.html | 12 +- .../includes/security_configuration.html | 77 --------- trustpoint/templates/pki/domains/config.html | 60 +++---- .../pki/issuing_cas/add/method_select.html | 39 +++++ 20 files changed, 439 insertions(+), 454 deletions(-) create mode 100644 trustpoint/pki/tests/test_views_issuing_cas_autogen.py diff --git a/trustpoint/management/forms.py b/trustpoint/management/forms.py index e17888e26..19517cfdf 100644 --- a/trustpoint/management/forms.py +++ b/trustpoint/management/forms.py @@ -47,7 +47,6 @@ from pki.models.truststore import TruststoreModel from pki.services.external_csr import certificate_matches_credential, parse_single_certificate from pki.services.key_generation import supported_key_type_choices -from pki.util.keys import AutoGenPkiKeyAlgorithm, supported_auto_gen_pki_key_algorithms from pki.util.x509 import CertificateVerifier from trustpoint.logger import LoggerMixin @@ -204,7 +203,7 @@ class SecurityConfigForm(forms.ModelForm[SecurityConfig]): """Security configuration model form.""" FEATURE_TO_FIELDS: ClassVar[dict[type[SecurityFeature], list[str]]] = { - AutoGenPkiFeature: ['auto_gen_pki', 'auto_gen_pki_key_algorithm'], + AutoGenPkiFeature: ['auto_gen_pki'], } def __init__(self, *args: Any, **kwargs: Any)-> None: @@ -229,26 +228,6 @@ def __init__(self, *args: Any, **kwargs: Any)-> None: if field_name in self.fields: self.fields[field_name].widget.attrs['disabled'] = 'disabled' - supported_algorithms = supported_auto_gen_pki_key_algorithms() - self.supported_auto_gen_pki_key_algorithms = supported_algorithms - - if self.instance and self.instance.auto_gen_pki: - self.fields['auto_gen_pki_key_algorithm'].widget.attrs['disabled'] = 'disabled' - elif 'auto_gen_pki_key_algorithm' in self.fields: - auto_gen_pki_key_algorithm_field = cast( - 'forms.ChoiceField', - self.fields['auto_gen_pki_key_algorithm'], - ) - if supported_algorithms: - auto_gen_pki_key_algorithm_field.choices = [ - (algorithm.value, algorithm.label) for algorithm in supported_algorithms - ] - else: - auto_gen_pki_key_algorithm_field.choices = [ - ('', _('No supported backend algorithms available')), - ] - auto_gen_pki_key_algorithm_field.widget.attrs['disabled'] = 'disabled' - self.helper = FormHelper() self.helper.layout = Layout( Fieldset( @@ -258,15 +237,22 @@ def __init__(self, *args: Any, **kwargs: Any)-> None: ), Fieldset( _('Advanced security settings'), - Field('auto_gen_pki', wrapper_class='form-check form-switch'), - 'auto_gen_pki_key_algorithm', 'rsa_minimum_key_size', 'max_cert_validity_days', 'max_crl_validity_days', 'credential_ttl_seconds', Field('allow_ca_issuance', wrapper_class='form-check form-switch'), - Field('allow_auto_gen_pki', wrapper_class='form-check form-switch'), Field('allow_self_signed_ca', wrapper_class='form-check form-switch'), + Field('auto_gen_pki', wrapper_class='form-check form-switch'), + HTML( + "{% load i18n %}" + "{% if form.instance.auto_gen_pki %}" + '

' + '' + "{% trans 'Create an auto-generated PKI' %}" + '

' + "{% endif %}" + ), Field('allow_imported_private_keys', wrapper_class='form-check form-switch'), 'permitted_no_onboarding_pki_protocols', 'permitted_onboarding_protocols' @@ -285,7 +271,10 @@ def __init__(self, *args: Any, **kwargs: Any)-> None: auto_gen_pki = forms.BooleanField( required=False, - label=_('Enable local auto-generated PKI'), + label=_('Allow local auto-generated PKI creation'), + help_text=_( + 'Turning this off disables the local auto-generated PKI and revokes all certificates issued by it.' + ), widget=forms.CheckboxInput( attrs={ 'class': 'form-check-input', @@ -297,13 +286,6 @@ def __init__(self, *args: Any, **kwargs: Any)-> None: ), ) - auto_gen_pki_key_algorithm = forms.ChoiceField( - choices=AutoGenPkiKeyAlgorithm, - label=_('Key Algorithm for auto-generated PKI'), - required=False, - widget=forms.Select(attrs={'data-hide-at-sl': '[false, false, true, true, true]'}), - ) - RSA_KEY_CHOICES: ClassVar[list[tuple[object, object]]] = [ ('', _('None / Not Permitted')), (1024, '1024'), @@ -343,10 +325,10 @@ class Meta: """Meta configuration for SecurityConfigForm.""" model = SecurityConfig fields: ClassVar[list[str]] = [ - 'security_mode', 'auto_gen_pki', 'auto_gen_pki_key_algorithm', + 'security_mode', 'rsa_minimum_key_size', 'max_cert_validity_days', 'max_crl_validity_days', - 'credential_ttl_seconds', - 'allow_ca_issuance', 'allow_auto_gen_pki', 'allow_self_signed_ca', + 'credential_ttl_seconds', 'auto_gen_pki', + 'allow_ca_issuance', 'allow_self_signed_ca', 'allow_imported_private_keys', 'permitted_no_onboarding_pki_protocols', 'permitted_onboarding_protocols' @@ -402,24 +384,6 @@ def clean_rsa_minimum_key_size(self) -> int | None: except (TypeError, ValueError) as err: raise ValidationError(_('Invalid RSA key size.')) from err - def clean_auto_gen_pki_key_algorithm(self) -> AutoGenPkiKeyAlgorithm: - """Keep the current value of `auto_gen_pki_key_algorithm` from the instance if the field was disabled.""" - form_value = self.cleaned_data.get('auto_gen_pki_key_algorithm') - if form_value is None or form_value == '': - if self.instance: - return AutoGenPkiKeyAlgorithm(self.instance.auto_gen_pki_key_algorithm) - return AutoGenPkiKeyAlgorithm.RSA2048 - selected_algorithm = AutoGenPkiKeyAlgorithm(form_value) - supported_algorithms = getattr( - self, - 'supported_auto_gen_pki_key_algorithms', - supported_auto_gen_pki_key_algorithms(), - ) - if selected_algorithm not in supported_algorithms: - msg = _('The selected auto-generated PKI algorithm is not supported by the active backend.') - raise ValidationError(msg) - return selected_algorithm - def _validate_mode_constraints(self, cleaned: dict[str, Any], mode: str) -> None: """Validate that submitted values comply with the given security mode defaults.""" defaults = SecurityConfig._MODE_DEFAULTS[mode] # noqa: SLF001 @@ -461,22 +425,15 @@ def clean(self) -> dict[str, Any]: if mode != SecurityConfig.SecurityModeChoices.LAB: self._validate_mode_constraints(cleaned, str(mode)) - if cleaned.get('auto_gen_pki') and not cleaned.get('allow_auto_gen_pki'): - self.add_error('auto_gen_pki', 'Cannot enable auto-generated PKI when it is not permitted.') + selected_mode = str(mode) + mode_defaults = SecurityConfig._MODE_DEFAULTS.get(selected_mode, {}) # noqa: SLF001 + if self.instance and selected_mode == self.instance.security_mode: + auto_gen_pki_allowed = self.instance.allow_auto_gen_pki + else: + auto_gen_pki_allowed = mode_defaults.get('allow_auto_gen_pki', False) - supported_algorithms = getattr( - self, - 'supported_auto_gen_pki_key_algorithms', - supported_auto_gen_pki_key_algorithms(), - ) - selected_algorithm = cleaned.get('auto_gen_pki_key_algorithm') - if cleaned.get('auto_gen_pki') and not supported_algorithms: - self.add_error('auto_gen_pki', _('No auto-generated PKI algorithm is supported by the active backend.')) - elif cleaned.get('auto_gen_pki') and selected_algorithm not in supported_algorithms: - self.add_error( - 'auto_gen_pki_key_algorithm', - _('The selected auto-generated PKI algorithm is not supported by the active backend.'), - ) + if cleaned.get('auto_gen_pki') and not auto_gen_pki_allowed: + self.add_error('auto_gen_pki', 'Cannot enable auto-generated PKI when it is not permitted.') return cleaned diff --git a/trustpoint/management/tests/test_forms/test_security_config.py b/trustpoint/management/tests/test_forms/test_security_config.py index c1456d16f..09235dca4 100644 --- a/trustpoint/management/tests/test_forms/test_security_config.py +++ b/trustpoint/management/tests/test_forms/test_security_config.py @@ -7,7 +7,6 @@ from management.forms import SecurityConfigForm from management.models import SecurityConfig from onboarding.enums import NoOnboardingPkiProtocol, OnboardingProtocol -from pki.util.keys import AutoGenPkiKeyAlgorithm class SecurityConfigFormTest(TestCase): @@ -18,7 +17,6 @@ def setUp(self): self.config = SecurityConfig.objects.create( security_mode=SecurityConfig.SecurityModeChoices.BROWNFIELD, auto_gen_pki=False, - auto_gen_pki_key_algorithm=AutoGenPkiKeyAlgorithm.RSA2048 ) def test_form_initialization_with_instance(self): @@ -26,7 +24,8 @@ def test_form_initialization_with_instance(self): form = SecurityConfigForm(instance=self.config) self.assertIn('security_mode', form.fields) self.assertIn('auto_gen_pki', form.fields) - self.assertIn('auto_gen_pki_key_algorithm', form.fields) + assert 'allow_auto_gen_pki' not in form.fields + assert 'auto_gen_pki_key_algorithm' not in form.fields assert 'allow_imported_private_keys' in form.fields def test_form_initialization_without_instance(self): @@ -66,7 +65,6 @@ def test_form_with_dev_security_mode(self): form_data = { 'security_mode': SecurityConfig.SecurityModeChoices.LAB, 'auto_gen_pki': True, - 'auto_gen_pki_key_algorithm': AutoGenPkiKeyAlgorithm.RSA2048, 'allow_auto_gen_pki': True, } form = SecurityConfigForm(data=form_data, instance=self.config) @@ -77,7 +75,6 @@ def test_form_with_high_security_mode(self): form_data = { 'security_mode': SecurityConfig.SecurityModeChoices.HARDENED, 'auto_gen_pki': False, - 'auto_gen_pki_key_algorithm': AutoGenPkiKeyAlgorithm.RSA2048, # Hardened defaults from _MODE_DEFAULTS 'rsa_minimum_key_size': 4096, 'max_cert_validity_days': 365, @@ -90,78 +87,6 @@ def test_form_with_high_security_mode(self): form = SecurityConfigForm(data=form_data, instance=self.config) self.assertTrue(form.is_valid()) - def test_clean_auto_gen_pki_key_algorithm_with_none(self): - """Test clean method returns instance value when form value is not provided.""" - self.config.auto_gen_pki = True - self.config.auto_gen_pki_key_algorithm = AutoGenPkiKeyAlgorithm.SECP256R1 - self.config.save() - - form_data = { - 'security_mode': SecurityConfig.SecurityModeChoices.BROWNFIELD, - 'auto_gen_pki': True, - 'auto_gen_pki_key_algorithm': AutoGenPkiKeyAlgorithm.SECP256R1, - # Brownfield defaults from _MODE_DEFAULTS - 'rsa_minimum_key_size': 1024, - 'max_cert_validity_days': 1825, - 'max_crl_validity_days': 365, - 'allow_ca_issuance': False, - 'allow_auto_gen_pki': True, - 'allow_self_signed_ca': True, - } - form = SecurityConfigForm(data=form_data, instance=self.config) - self.assertTrue(form.is_valid()) - result = form.cleaned_data['auto_gen_pki_key_algorithm'] - self.assertEqual(result, AutoGenPkiKeyAlgorithm.SECP256R1) - - def test_clean_auto_gen_pki_key_algorithm_returns_provided_value(self): - """Test clean method uses the provided algorithm value.""" - form_data = { - 'security_mode': SecurityConfig.SecurityModeChoices.BROWNFIELD, - 'auto_gen_pki': True, - 'auto_gen_pki_key_algorithm': AutoGenPkiKeyAlgorithm.RSA2048, - # Brownfield defaults from _MODE_DEFAULTS - 'rsa_minimum_key_size': 1024, - 'max_cert_validity_days': 1825, - 'max_crl_validity_days': 365, - 'allow_ca_issuance': False, - 'allow_auto_gen_pki': True, - 'allow_self_signed_ca': True, - } - form = SecurityConfigForm(data=form_data) - self.assertTrue(form.is_valid()) - result = form.cleaned_data['auto_gen_pki_key_algorithm'] - self.assertEqual(result, AutoGenPkiKeyAlgorithm.RSA2048) - - def test_clean_auto_gen_pki_key_algorithm_with_value(self): - """Test clean method uses provided value when available.""" - form_data = { - 'security_mode': SecurityConfig.SecurityModeChoices.BROWNFIELD, - 'auto_gen_pki': True, - 'auto_gen_pki_key_algorithm': AutoGenPkiKeyAlgorithm.RSA4096, - # Brownfield defaults from _MODE_DEFAULTS - 'rsa_minimum_key_size': 1024, - 'max_cert_validity_days': 1825, - 'max_crl_validity_days': 365, - 'allow_ca_issuance': False, - 'allow_auto_gen_pki': True, - 'allow_self_signed_ca': True, - } - form = SecurityConfigForm(data=form_data, instance=self.config) - self.assertTrue(form.is_valid()) - result = form.cleaned_data['auto_gen_pki_key_algorithm'] - self.assertEqual(result, AutoGenPkiKeyAlgorithm.RSA4096) - - def test_form_disables_algorithm_field_when_auto_gen_pki_enabled(self): - """Test that algorithm field is disabled when auto_gen_pki is already enabled.""" - self.config.auto_gen_pki = True - self.config.save() - - form = SecurityConfigForm(instance=self.config) - self.assertEqual( - form.fields['auto_gen_pki_key_algorithm'].widget.attrs.get('disabled'), - 'disabled' - ) - def test_form_initialization_with_data_security_mode(self): """Test form initialization considers security_mode from form data.""" form_data = { @@ -185,7 +110,6 @@ def test_all_security_modes(self): form_data = { 'security_mode': mode, 'auto_gen_pki': defaults['allow_auto_gen_pki'], - 'auto_gen_pki_key_algorithm': AutoGenPkiKeyAlgorithm.RSA2048, 'rsa_minimum_key_size': defaults['rsa_minimum_key_size'] or '', 'max_cert_validity_days': defaults['max_cert_validity_days'], 'max_crl_validity_days': defaults['max_crl_validity_days'], @@ -209,7 +133,6 @@ def test_form_saves_imported_private_key_policy(self) -> None: data={ 'security_mode': SecurityConfig.SecurityModeChoices.LAB, 'auto_gen_pki': False, - 'auto_gen_pki_key_algorithm': AutoGenPkiKeyAlgorithm.RSA2048, 'allow_imported_private_keys': True, }, instance=self.config, @@ -252,7 +175,6 @@ def test_protocol_allowlists_are_saved_as_int_lists() -> None: config = SecurityConfig.objects.create( security_mode=SecurityConfig.SecurityModeChoices.LAB, auto_gen_pki=False, - auto_gen_pki_key_algorithm=AutoGenPkiKeyAlgorithm.RSA2048, ) no_onboarding_values = [ @@ -268,7 +190,6 @@ def test_protocol_allowlists_are_saved_as_int_lists() -> None: data={ 'security_mode': SecurityConfig.SecurityModeChoices.LAB, 'auto_gen_pki': False, - 'auto_gen_pki_key_algorithm': AutoGenPkiKeyAlgorithm.RSA2048, 'permitted_no_onboarding_pki_protocols': no_onboarding_values, 'permitted_onboarding_protocols': onboarding_values, }, diff --git a/trustpoint/management/tests/test_views/test_backend_configuration.py b/trustpoint/management/tests/test_views/test_backend_configuration.py index 5a7a5e96a..b3bf3b5b6 100644 --- a/trustpoint/management/tests/test_views/test_backend_configuration.py +++ b/trustpoint/management/tests/test_views/test_backend_configuration.py @@ -73,7 +73,7 @@ def test_get_context_data_with_software_config(self) -> None: assert context['is_software_backend'] assert context['capability_badge'] == 'success' assert context['supported_key_capabilities'] - assert context['supported_auto_gen_pki_algorithms'] + assert context['supported_auto_gen_pki_key_types'] assert 'page_title' in context def test_get_context_data_with_softhsm_config(self) -> None: diff --git a/trustpoint/management/tests/test_views/test_settings.py b/trustpoint/management/tests/test_views/test_settings.py index 5c82980ab..c4da34b31 100644 --- a/trustpoint/management/tests/test_views/test_settings.py +++ b/trustpoint/management/tests/test_views/test_settings.py @@ -16,7 +16,7 @@ from management.forms import SecurityConfigForm, SmtpEmailConfigForm, SmtpEmailTestForm from management.models import LoggingConfig, SecurityConfig, SmtpEmailConfig from management.views.settings import ChangeLogLevelView, SecuritySettingsView, SettingsTabView, MetricsSettingsView -from pki.util.keys import AutoGenPkiKeyAlgorithm +from pki.models import CaModel, DomainModel LOG_LEVELS = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] @@ -154,8 +154,8 @@ def test_form_valid_resets_settings_on_security_mode_increase(self, mock_apply): @patch('management.security.features.AutoGenPkiFeature.enable') @patch.object(SecurityConfig, 'apply_security_settings') - def test_form_valid_enables_auto_gen_pki(self, mock_apply, mock_enable): - """Test form_valid enables AutoGenPkiFeature when auto_gen_pki is enabled.""" + def test_form_valid_does_not_generate_auto_gen_pki_when_allowed(self, mock_apply, mock_enable): + """Allowing AutoGenPKI in settings does not create it.""" mock_sec = Mock() mock_sec.enable_feature = Mock() self.view.sec = mock_sec @@ -166,13 +166,15 @@ def test_form_valid_enables_auto_gen_pki(self, mock_apply, mock_enable): form.changed_data = ['auto_gen_pki'] form.cleaned_data = { 'auto_gen_pki': True, - 'auto_gen_pki_key_algorithm': AutoGenPkiKeyAlgorithm.RSA2048, } form.save = Mock() self.view.form_valid(form) - mock_sec.enable_feature.assert_called_once() + mock_enable.assert_not_called() + mock_sec.enable_feature.assert_not_called() + self.assertFalse(CaModel.objects.exists()) + self.assertFalse(DomainModel.objects.exists()) @patch('management.security.features.AutoGenPkiFeature.disable') @patch.object(SecurityConfig, 'apply_security_settings') @@ -225,24 +227,6 @@ def test_form_valid_handles_missing_security_mode(self): messages_list = list(get_messages(self.view.request)) self.assertTrue(any('missing' in str(msg).lower() for msg in messages_list)) - def test_form_valid_handles_missing_key_algorithm(self): - """Test form_valid handles missing key algorithm when enabling auto_gen_pki.""" - form = Mock(spec=SecurityConfigForm) - form.instance = self.security_config - form.instance.pk = 1 - form.changed_data = ['auto_gen_pki'] - form.cleaned_data = { - 'auto_gen_pki': True, - 'auto_gen_pki_key_algorithm': None, - } - form.save = Mock() - - with patch.object(SecurityConfig, 'apply_security_settings'): - self.view.form_valid(form) - - messages_list = list(get_messages(self.view.request)) - self.assertTrue(any('missing' in str(msg).lower() for msg in messages_list)) - def test_form_invalid_shows_error_message(self): """Test form_invalid displays error message.""" form = Mock(spec=SecurityConfigForm) @@ -264,7 +248,6 @@ def test_form_invalid_renders_full_settings_page(self): data={ 'security_mode': SecurityConfig.SecurityModeChoices.HARDENED, 'auto_gen_pki': False, - 'auto_gen_pki_key_algorithm': AutoGenPkiKeyAlgorithm.RSA2048, 'rsa_minimum_key_size': 1024, 'max_cert_validity_days': 365, 'max_crl_validity_days': 90, diff --git a/trustpoint/management/views/backend_configuration.py b/trustpoint/management/views/backend_configuration.py index 8965cc7d8..bb6bcec5d 100644 --- a/trustpoint/management/views/backend_configuration.py +++ b/trustpoint/management/views/backend_configuration.py @@ -20,7 +20,7 @@ from appsecrets.models import AppSecretBackendModel from crypto.application.capabilities import BackendCapabilityReport, get_active_backend_capability_report from crypto.models import BackendKind, CryptoProviderProfileModel -from pki.util.keys import supported_auto_gen_pki_key_algorithms +from pki.services.key_generation import supported_key_type_choices PKCS11_ASSET_DOWNLOADS = { 'module': ('module_path', 'application/octet-stream'), @@ -258,7 +258,7 @@ def get_context_data(self, **kwargs: Any) -> dict[str, Any]: context['capability_message'] = capability_message context['capability_diagnostics'] = capability_report.diagnostics if capability_report is not None else () context['supported_key_capabilities'] = _supported_key_capabilities(capability_report) - context['supported_auto_gen_pki_algorithms'] = supported_auto_gen_pki_key_algorithms() + context['supported_auto_gen_pki_key_types'] = supported_key_type_choices() pkcs11_capability_payload = ( getattr(pkcs11_probe_detail, 'snapshot_payload', None) if pkcs11_probe_detail is not None diff --git a/trustpoint/management/views/settings.py b/trustpoint/management/views/settings.py index 077276141..5892dd69a 100644 --- a/trustpoint/management/views/settings.py +++ b/trustpoint/management/views/settings.py @@ -46,7 +46,6 @@ from management.models.workflows2 import WorkflowExecutionConfig from management.security.features import AutoGenPkiFeature from management.security.mixins import SecurityLevelMixin -from pki.util.keys import AutoGenPkiKeyAlgorithm from trustpoint.logger import LoggerMixin from trustpoint.page_context import PageContextMixin from trustpoint.views.base import UserPermissionRequiredMixin @@ -623,17 +622,7 @@ def form_valid(self, form: SecurityConfigForm) -> HttpResponse: new_auto = form.cleaned_data.get('auto_gen_pki', None) self.logger.info('auto_gen_pki changed: old=%s, new=%s', old_auto, new_auto) - if old_auto != new_auto and new_auto: - key_alg_value = form.cleaned_data.get('auto_gen_pki_key_algorithm') - if key_alg_value is None: - messages.error(self.request, 'Auto-generated PKI key algorithm is missing.') - return redirect(self.success_url) - key_alg = AutoGenPkiKeyAlgorithm(key_alg_value) - self.logger.info('Calling enable_feature for AutoGenPkiFeature with key_alg: %s', key_alg) - self.sec.enable_feature(AutoGenPkiFeature, {'key_algorithm': key_alg}) - self.logger.info('Auto-generated PKI enabled with key algorithm: %s', key_alg.name) - - elif old_auto != new_auto and not new_auto: + if old_auto != new_auto and not new_auto: AutoGenPkiFeature.disable() self.logger.info('Auto-generated PKI disabled') diff --git a/trustpoint/pki/auto_gen_pki.py b/trustpoint/pki/auto_gen_pki.py index 789ba8df1..7a77edadb 100644 --- a/trustpoint/pki/auto_gen_pki.py +++ b/trustpoint/pki/auto_gen_pki.py @@ -15,11 +15,10 @@ ManagedRSAPrivateKey, generate_managed_signing_private_key, ) -from crypto.domain.algorithms import EllipticCurveName -from crypto.domain.specs import EcKeySpec, KeySpec, MlDsaKeySpec, MlDsaVariant, RsaKeySpec from crypto.models import CryptoManagedKeyModel from pki.models import CaModel, CredentialModel, DomainModel, RevokedCertificateModel -from pki.util.keys import AutoGenPkiKeyAlgorithm, supported_auto_gen_pki_key_algorithms +from pki.services.key_generation import key_spec_for_key_type, supported_key_type_choices +from pki.util.keys import AutoGenPkiKeyAlgorithm from pki.util.x509 import CertificateGenerator from trustpoint.logger import LoggerMixin @@ -37,36 +36,39 @@ class AutoGenPki(LoggerMixin): _lock: threading.Lock = threading.Lock() @staticmethod - def _key_spec_for_algorithm(key_alg: AutoGenPkiKeyAlgorithm) -> KeySpec: - """Map AutoGenPKI choices to the backend key-generation contract.""" - if key_alg == AutoGenPkiKeyAlgorithm.RSA2048: - return RsaKeySpec(key_size=2048) - if key_alg == AutoGenPkiKeyAlgorithm.RSA4096: - return RsaKeySpec(key_size=4096) - if key_alg == AutoGenPkiKeyAlgorithm.SECP256R1: - return EcKeySpec(curve=EllipticCurveName.SECP256R1) - if key_alg == AutoGenPkiKeyAlgorithm.MLDSA44: - return MlDsaKeySpec(variant=MlDsaVariant.MLDSA44) - if key_alg == AutoGenPkiKeyAlgorithm.MLDSA65: - return MlDsaKeySpec(variant=MlDsaVariant.MLDSA65) - if key_alg == AutoGenPkiKeyAlgorithm.MLDSA87: - return MlDsaKeySpec(variant=MlDsaVariant.MLDSA87) - msg = f'Unsupported AutoGenPKI key algorithm {key_alg!r}.' - raise ValueError(msg) + def _normalize_key_type(key_type: str | AutoGenPkiKeyAlgorithm) -> str: + """Convert the legacy algorithm enum to the shared key-generation service format.""" + if isinstance(key_type, AutoGenPkiKeyAlgorithm): + name = key_type.name + if name.startswith('RSA'): + return f'RSA-{name.removeprefix("RSA")}' + if name.startswith('SECP'): + return f'ECC-{name}' + if name.startswith('MLDSA'): + return f'MLDSA-{name.removeprefix("MLDSA")}' + msg = f'Unsupported AutoGenPKI key algorithm {key_type!r}.' + raise ValueError(msg) + return key_type + + @classmethod + def _key_type_identifier(cls, key_type: str | AutoGenPkiKeyAlgorithm) -> str: + """Return a stable name suffix for Root CA, Issuing CA and Domain records.""" + return cls._normalize_key_type(key_type).replace('-', '') @staticmethod def _generate_private_key( - key_alg: AutoGenPkiKeyAlgorithm, + key_type: str, key_label: str, ) -> ManagedRSAPrivateKey | ManagedECPrivateKey | ManagedMLDSAPrivateKey: """Generate an AutoGenPKI key in the active backend.""" - if key_alg not in supported_auto_gen_pki_key_algorithms(): - msg = f'The active crypto backend does not support AutoGenPKI algorithm {key_alg.label}.' + supported_types = {value for value, _label in supported_key_type_choices()} + if key_type not in supported_types: + msg = f'Unsupported AutoGenPKI key type for the active crypto backend: {key_type}.' raise ValueError(msg) return generate_managed_signing_private_key( alias=key_label, - key_spec=AutoGenPki._key_spec_for_algorithm(key_alg), + key_spec=key_spec_for_key_type(key_type), ) @staticmethod @@ -105,11 +107,11 @@ def _save_managed_issuing_ca( # noqa: PLR0913 return ca @classmethod - def get_auto_gen_pki(cls, key_alg: AutoGenPkiKeyAlgorithm | None = None) -> CaModel | None: + def get_auto_gen_pki(cls, key_type: str | AutoGenPkiKeyAlgorithm | None = None) -> CaModel | None: """Retrieves the auto-generated PKI Issuing CA, if it exists.""" - if key_alg is not None: + if key_type is not None: return CaModel.objects.filter( - unique_name__startswith=f'{UNIQUE_NAME_PREFIX}_{key_alg.name}', + unique_name__startswith=f'{UNIQUE_NAME_PREFIX}_{cls._key_type_identifier(key_type)}', ca_type=CaModel.CaTypeChoice.AUTOGEN, is_active=True, ).first() @@ -120,25 +122,35 @@ def get_auto_gen_pki(cls, key_alg: AutoGenPkiKeyAlgorithm | None = None) -> CaMo ).first() @classmethod - def enable_auto_gen_pki(cls, key_alg: AutoGenPkiKeyAlgorithm) -> None: + def enable_auto_gen_pki( + cls, + key_type: str | AutoGenPkiKeyAlgorithm | None = None, + *, + key_alg: AutoGenPkiKeyAlgorithm | None = None, + ) -> CaModel | None: """Enables the auto-generated PKI.""" + key_type = key_type or key_alg + if key_type is None: + raise ValueError('An AutoGenPKI key type is required.') + key_type = cls._normalize_key_type(key_type) + key_identifier = cls._key_type_identifier(key_type) with cls._lock: - cls.logger.warning('! Enabling auto-generated PKI with key algorithm: %s !', key_alg.name) + cls.logger.warning('! Enabling auto-generated PKI with key type: %s !', key_type) unique_suffix = secrets.token_hex(4) - issuing_ca_unique_name = f'{UNIQUE_NAME_PREFIX}_{key_alg.name}_{unique_suffix}' - domain_unique_name = f'{DOMAIN_NAME_PREFIX}_{key_alg.name}' + issuing_ca_unique_name = f'{UNIQUE_NAME_PREFIX}_{key_identifier}_{unique_suffix}' + domain_unique_name = f'{DOMAIN_NAME_PREFIX}_{key_identifier}' - existing_issuing_ca = cls.get_auto_gen_pki(key_alg) + existing_issuing_ca = cls.get_auto_gen_pki() if existing_issuing_ca: cls.logger.error( - 'Issuing CA for auto-generated PKI already exists: %s - ' + 'An active auto-generated PKI already exists: %s - ' 'auto-generated PKI was possibly not correctly disabled', existing_issuing_ca.unique_name ) - return + return None - root_ca_name = f'AutoGenPKI_Root_CA_{key_alg.name}' + root_ca_name = f'AutoGenPKI_Root_CA_{key_identifier}' # Re-use any existing root CA for the auto-generated PKI and current key type try: root_ca = CaModel.objects.get( @@ -149,7 +161,7 @@ def enable_auto_gen_pki(cls, key_alg: AutoGenPkiKeyAlgorithm) -> None: cls.logger.info('Reusing existing Root CA: %s', root_ca_name) except CaModel.DoesNotExist: cls.logger.info('Creating new Root CA: %s', root_ca_name) - root_private_key = cls._generate_private_key(key_alg, f'{root_ca_name}_{unique_suffix}') + root_private_key = cls._generate_private_key(key_type, f'{root_ca_name}_{unique_suffix}') root_cert, _ = CertificateGenerator.create_root_ca( root_ca_name, private_key=root_private_key, # type: ignore[arg-type] @@ -165,7 +177,7 @@ def enable_auto_gen_pki(cls, key_alg: AutoGenPkiKeyAlgorithm) -> None: cls.logger.info('Created new Root CA: %s', root_ca_name) cls.logger.info('Creating new Issuing CA with unique name: %s', issuing_ca_unique_name) - issuing_private_key = cls._generate_private_key(key_alg, issuing_ca_unique_name) + issuing_private_key = cls._generate_private_key(key_type, issuing_ca_unique_name) issuing_1, _ = CertificateGenerator.create_issuing_ca( root_1_key, root_ca_name, @@ -195,7 +207,8 @@ def enable_auto_gen_pki(cls, key_alg: AutoGenPkiKeyAlgorithm) -> None: domain.save() cls.logger.info('Domain %s updated and activated', domain_unique_name) - cls.logger.warning('Auto-generated PKI enabled with key algorithm: %s', key_alg.name) + cls.logger.warning('Auto-generated PKI enabled with key type: %s', key_type) + return issuing_ca @classmethod def disable_auto_gen_pki(cls) -> None: @@ -205,7 +218,7 @@ def disable_auto_gen_pki(cls) -> None: Managed backend keys are not destroyed - each Issuing CA has a unique name to avoid conflicts. """ with cls._lock: - issuing_ca = cls.get_auto_gen_pki(key_alg=None) + issuing_ca = cls.get_auto_gen_pki(key_type=None) if not issuing_ca: cls.logger.error( 'Issuing CA for auto-generated PKI does not exist - auto-generated PKI possibly not fully disabled' diff --git a/trustpoint/pki/forms/__init__.py b/trustpoint/pki/forms/__init__.py index 5ae4a765f..325a4cdeb 100644 --- a/trustpoint/pki/forms/__init__.py +++ b/trustpoint/pki/forms/__init__.py @@ -8,6 +8,7 @@ from .csr import CsrIssuanceForm from .devids import DevIdAddMethodSelectForm, DevIdRegistrationForm from .issuing_cas import ( + IssuingCaAddAutoGenForm, IssuingCaAddFileImportPkcs12Form, IssuingCaAddFileImportSeparateFilesForm, IssuingCaAddMethodSelectForm, @@ -29,6 +30,7 @@ from .truststores import TruststoreAddForm, TruststoreDownloadForm __all__ = [ + 'IssuingCaAddAutoGenForm', 'CertProfileConfigForm', 'CertificateDownloadForm', 'CertificateIssuanceForm', diff --git a/trustpoint/pki/forms/issuing_cas.py b/trustpoint/pki/forms/issuing_cas.py index 2df99eebf..9e9775cc9 100644 --- a/trustpoint/pki/forms/issuing_cas.py +++ b/trustpoint/pki/forms/issuing_cas.py @@ -305,6 +305,34 @@ class IssuingCaAddMethodSelectForm(forms.Form): ) +class IssuingCaAddAutoGenForm(forms.Form): + """Form for generating the local auto-generated PKI.""" + + key_type = forms.ChoiceField( + label=_('Key Type'), + choices=KEY_TYPE_CHOICES, + initial='RSA-2048', + required=True, + widget=forms.Select(attrs={'class': 'form-select'}), + ) + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Use only key types supported by the active crypto backend.""" + super().__init__(*args, **kwargs) + choices = supported_key_type_choices() + self.fields['key_type'].choices = choices or [('', _('No supported backend algorithms available'))] + if not choices: + self.fields['key_type'].widget.attrs['disabled'] = 'disabled' + + def clean_key_type(self) -> str: + """Reject key types that the active backend cannot generate.""" + key_type = self.cleaned_data['key_type'] + supported_types = {value for value, _label in supported_key_type_choices()} + if key_type not in supported_types: + raise ValidationError(_('The active crypto backend does not support this key type.')) + return key_type + + class IssuingCaFileTypeSelectForm(forms.Form): """Form for selecting the file type when importing an Issuing CA. diff --git a/trustpoint/pki/tests/test_auto_gen_pki.py b/trustpoint/pki/tests/test_auto_gen_pki.py index 709091b30..00893b020 100644 --- a/trustpoint/pki/tests/test_auto_gen_pki.py +++ b/trustpoint/pki/tests/test_auto_gen_pki.py @@ -3,7 +3,6 @@ """Tests for the auto-generated PKI.""" -from typing import cast from unittest import mock import pytest @@ -93,23 +92,22 @@ def disable_auto_gen_pki() -> None: @pytest.mark.parametrize( - ('key_alg', 'expected_type'), + ('key_alg', 'expected_key_type'), [ - (AutoGenPkiKeyAlgorithm.RSA2048, 'RsaKeySpec'), - (AutoGenPkiKeyAlgorithm.RSA4096, 'RsaKeySpec'), - (AutoGenPkiKeyAlgorithm.SECP256R1, 'EcKeySpec'), - (AutoGenPkiKeyAlgorithm.MLDSA44, 'MlDsaKeySpec'), - (AutoGenPkiKeyAlgorithm.MLDSA65, 'MlDsaKeySpec'), - (AutoGenPkiKeyAlgorithm.MLDSA87, 'MlDsaKeySpec'), + (AutoGenPkiKeyAlgorithm.RSA2048, 'RSA-2048'), + (AutoGenPkiKeyAlgorithm.RSA4096, 'RSA-4096'), + (AutoGenPkiKeyAlgorithm.SECP256R1, 'ECC-SECP256R1'), + (AutoGenPkiKeyAlgorithm.MLDSA44, 'MLDSA-44'), + (AutoGenPkiKeyAlgorithm.MLDSA65, 'MLDSA-65'), + (AutoGenPkiKeyAlgorithm.MLDSA87, 'MLDSA-87'), ], ) -def test_key_spec_for_supported_algorithms(key_alg: AutoGenPkiKeyAlgorithm, expected_type: str) -> None: - """Each public AutoGenPKI algorithm maps to its backend key specification.""" - key_spec_for_algorithm = getattr(AutoGenPki, '_key_spec_for_algorithm') - assert type(key_spec_for_algorithm(key_alg)).__name__ == expected_type +def test_legacy_algorithm_normalizes_to_shared_key_type(key_alg: AutoGenPkiKeyAlgorithm, expected_key_type: str) -> None: + """Legacy AutoGen callers normalize to the shared key-generation service format.""" + assert AutoGenPki._normalize_key_type(key_alg) == expected_key_type def test_key_spec_for_unknown_algorithm_rejects_invalid_value() -> None: """Unknown AutoGenPKI choices fail before backend interaction.""" with pytest.raises(ValueError, match='Unsupported'): - getattr(AutoGenPki, '_key_spec_for_algorithm')(cast(AutoGenPkiKeyAlgorithm, 'invalid')) + AutoGenPki._generate_private_key('invalid', 'test-key') diff --git a/trustpoint/pki/tests/test_auto_gen_pki_lifecycle.py b/trustpoint/pki/tests/test_auto_gen_pki_lifecycle.py index 90d1c7c61..06d80b8cd 100644 --- a/trustpoint/pki/tests/test_auto_gen_pki_lifecycle.py +++ b/trustpoint/pki/tests/test_auto_gen_pki_lifecycle.py @@ -13,17 +13,19 @@ from pki.auto_gen_pki import DOMAIN_NAME_PREFIX, UNIQUE_NAME_PREFIX, AutoGenPki from pki.models import CaModel, DomainModel from pki.models.certificate import CertificateModel -from pki.util.keys import AutoGenPkiKeyAlgorithm, supported_auto_gen_pki_key_algorithms +from pki.services.key_generation import supported_key_type_choices +from pki.util.keys import AutoGenPkiKeyAlgorithm pytestmark = pytest.mark.django_db KEY_ALGORITHM = AutoGenPkiKeyAlgorithm.RSA2048 +KEY_TYPE = 'RSA-2048' @pytest.fixture(autouse=True) def _require_backend_support() -> None: """Skip when the active crypto backend cannot generate the test key.""" - if KEY_ALGORITHM not in supported_auto_gen_pki_key_algorithms(): + if KEY_TYPE not in {value for value, _label in supported_key_type_choices()}: pytest.skip('Active crypto backend does not support RSA-2048 AutoGenPKI keys.') diff --git a/trustpoint/pki/tests/test_util_keys_extended.py b/trustpoint/pki/tests/test_util_keys_extended.py index ff1a87198..9d80d650e 100644 --- a/trustpoint/pki/tests/test_util_keys_extended.py +++ b/trustpoint/pki/tests/test_util_keys_extended.py @@ -5,9 +5,6 @@ from __future__ import annotations -from types import SimpleNamespace -from unittest.mock import patch - import pytest from cryptography.hazmat.primitives import hashes from trustpoint_core.oid import NamedCurve, PublicKeyAlgorithmOid @@ -15,7 +12,6 @@ from pki.util.keys import ( AutoGenPkiKeyAlgorithm, CryptographyUtils, - supported_auto_gen_pki_key_algorithms, ) pytestmark = pytest.mark.django_db @@ -46,65 +42,6 @@ def test_rsa_and_ec_variants_carry_their_parameters(self) -> None: ) -class TestSupportedAlgorithms: - """Discovery of algorithms supported by the active crypto backend.""" - - def _report(self, **overrides: object) -> SimpleNamespace: - report = { - 'available': True, - 'backend_kind': 'software', - 'diagnostics': [], - 'supports_rsa_key_size': lambda _size: True, - 'supports_ec_curve': lambda _curve: True, - 'supports_key_spec': lambda _spec: True, - } - report.update(overrides) - return SimpleNamespace(**report) - - def test_unavailable_backend_supports_nothing(self) -> None: - """An unavailable backend offers no AutoGenPKI algorithms.""" - report = self._report(available=False, diagnostics=['backend offline']) - - with patch('crypto.application.capabilities.get_active_backend_capability_report', return_value=report): - assert supported_auto_gen_pki_key_algorithms() == () - - def test_software_backend_offers_classical_and_mldsa(self) -> None: - """A capable software backend offers RSA, EC and ML-DSA algorithms.""" - with patch( - 'crypto.application.capabilities.get_active_backend_capability_report', - return_value=self._report(), - ): - supported = supported_auto_gen_pki_key_algorithms() - - assert AutoGenPkiKeyAlgorithm.RSA2048 in supported - assert AutoGenPkiKeyAlgorithm.SECP256R1 in supported - assert AutoGenPkiKeyAlgorithm.MLDSA44 in supported - - def test_hardware_backend_omits_mldsa(self) -> None: - """Non-software backends do not offer ML-DSA algorithms.""" - with patch( - 'crypto.application.capabilities.get_active_backend_capability_report', - return_value=self._report(backend_kind='pkcs11'), - ): - supported = supported_auto_gen_pki_key_algorithms() - - assert AutoGenPkiKeyAlgorithm.RSA2048 in supported - assert AutoGenPkiKeyAlgorithm.MLDSA44 not in supported - - def test_unsupported_rsa_sizes_are_excluded(self) -> None: - """RSA variants the backend cannot generate are not offered.""" - report = self._report( - supports_rsa_key_size=lambda size: size == 2048, - supports_ec_curve=lambda _curve: False, - supports_key_spec=lambda _spec: False, - ) - - with patch('crypto.application.capabilities.get_active_backend_capability_report', return_value=report): - supported = supported_auto_gen_pki_key_algorithms() - - assert supported == (AutoGenPkiKeyAlgorithm.RSA2048,) - - class TestHashAlgorithmSelection: """Hash algorithm selection for signing keys.""" diff --git a/trustpoint/pki/tests/test_views_issuing_cas_autogen.py b/trustpoint/pki/tests/test_views_issuing_cas_autogen.py new file mode 100644 index 000000000..adc7a1cf8 --- /dev/null +++ b/trustpoint/pki/tests/test_views_issuing_cas_autogen.py @@ -0,0 +1,152 @@ +# Copyright (c) 2026 The Trustpoint Project Authors +# SPDX-License-Identifier: MIT + +"""Tests for AutoGenPKI generation from the Add Issuing CA page.""" + +from __future__ import annotations + +from unittest.mock import Mock, patch + +import pytest +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Permission +from django.contrib.messages import get_messages +from django.test import Client +from django.urls import reverse + +from management.models.audit_log import AuditLog +from management.models.security import SecurityConfig +from pki.auto_gen_pki import AutoGenPki +from pki.forms.issuing_cas import IssuingCaAddAutoGenForm +from pki.models import CaModel, DomainModel + + +@pytest.fixture +def manage_cas_client(db: None) -> Client: + """Return an authenticated client allowed to manage CAs.""" + user = get_user_model().objects.create_user(username='autogen-admin', password='test-password') + permission = Permission.objects.get(codename='manage_cas') + user.role.permissions.add(permission) + client = Client() + client.force_login(user) + return client + + +def _security_config(*, allowed: bool) -> SecurityConfig: + return SecurityConfig.objects.create( + security_mode=SecurityConfig.SecurityModeChoices.LAB, + auto_gen_pki=allowed, + allow_auto_gen_pki=True, + ) + + +@pytest.mark.django_db +def test_add_issuing_ca_page_keeps_autogen_section_visible_when_disabled(manage_cas_client: Client) -> None: + """The separate section remains visible but its action is disabled by settings.""" + _security_config(allowed=False) + + response = manage_cas_client.get(reverse('pki:issuing_cas-add-method_select')) + + assert response.status_code == 200 + assert b'Auto-generated PKI' in response.content + assert b'Auto-generated PKI creation is disabled in the Security Settings.' in response.content + assert response.context['auto_gen_pki_form'].fields['key_type'].widget.attrs['disabled'] == 'disabled' + assert response.context['can_generate_auto_gen_pki'] is False + + +@pytest.mark.django_db +def test_add_issuing_ca_page_disables_action_when_an_autogen_pki_is_active(manage_cas_client: Client) -> None: + """The active PKI is linked and cannot be generated again from the page.""" + _security_config(allowed=True) + active_ca = Mock(pk=42) + + with patch.object(AutoGenPki, 'get_auto_gen_pki', return_value=active_ca): + response = manage_cas_client.get(reverse('pki:issuing_cas-add-method_select')) + + assert response.status_code == 200 + assert b'An auto-generated PKI is already active.' in response.content + assert b'/pki/issuing-cas/detail/42/' in response.content + assert response.context['can_generate_auto_gen_pki'] is False + + +def test_autogen_form_uses_central_key_generation_choices() -> None: + """The AutoGen form takes its backend-filtered choices from key_generation.""" + choices = [('ECC-SECP384R1', 'ECC SECP384R1')] + with patch('pki.forms.issuing_cas.supported_key_type_choices', return_value=choices): + form = IssuingCaAddAutoGenForm() + + assert list(form.fields['key_type'].choices) == choices + + +@pytest.mark.django_db +def test_autogen_endpoint_rejects_user_without_manage_cas(db: None) -> None: + """Only users with MANAGE_CAS can submit the generation endpoint.""" + user = get_user_model().objects.create_user(username='autogen-reader', password='test-password') + client = Client() + client.force_login(user) + _security_config(allowed=True) + client.raise_request_exception = False + + response = client.post(reverse('pki:issuing_cas-add-autogen'), {'key_type': 'RSA-2048'}) + + assert response.status_code == 403 + assert not CaModel.objects.filter(ca_type=CaModel.CaTypeChoice.AUTOGEN).exists() + + +@pytest.mark.django_db +def test_autogen_endpoint_rejects_when_security_settings_disallow_creation(manage_cas_client: Client) -> None: + """The server rejects a forged POST even when the page's control is disabled.""" + _security_config(allowed=False) + + with patch.object(AutoGenPki, 'enable_auto_gen_pki') as generate: + response = manage_cas_client.post(reverse('pki:issuing_cas-add-autogen'), {'key_type': 'RSA-2048'}) + + assert response.status_code == 302 + generate.assert_not_called() + + +@pytest.mark.django_db +def test_autogen_endpoint_rejects_duplicate_active_pki(manage_cas_client: Client) -> None: + """The server blocks duplicate generation even for a direct POST.""" + _security_config(allowed=True) + + with ( + patch.object(AutoGenPki, 'get_auto_gen_pki', return_value=Mock()), + patch.object(AutoGenPki, 'enable_auto_gen_pki') as generate, + ): + response = manage_cas_client.post(reverse('pki:issuing_cas-add-autogen'), {'key_type': 'RSA-2048'}) + + assert response.status_code == 302 + generate.assert_not_called() + + +@pytest.mark.django_db +def test_autogen_endpoint_rejects_unsupported_key_type(manage_cas_client: Client) -> None: + """A key type absent from the backend-filtered form choices cannot be submitted.""" + _security_config(allowed=True) + + with patch.object(AutoGenPki, 'enable_auto_gen_pki') as generate: + response = manage_cas_client.post(reverse('pki:issuing_cas-add-autogen'), {'key_type': 'not-a-key-type'}) + + assert response.status_code == 302 + generate.assert_not_called() + + +@pytest.mark.django_db +def test_autogen_endpoint_generates_cas_domain_audit_and_success_message(manage_cas_client: Client) -> None: + """A valid POST generates the lifecycle and redirects with an audit entry.""" + _security_config(allowed=True) + + response = manage_cas_client.post(reverse('pki:issuing_cas-add-autogen'), {'key_type': 'RSA-2048'}) + + assert response.status_code == 302 + assert response.url == reverse('pki:issuing_cas') + issuing_ca = AutoGenPki.get_auto_gen_pki() + assert issuing_ca is not None + assert issuing_ca.parent_ca is not None + assert CaModel.objects.filter(ca_type=CaModel.CaTypeChoice.AUTOGEN_ROOT).count() == 1 + domain = DomainModel.objects.get(issuing_ca=issuing_ca) + assert domain.is_active + audit_entry = AuditLog.objects.get(operation_type=AuditLog.OperationType.CA_CREATED) + assert audit_entry.target == issuing_ca + assert any('Successfully generated auto-generated PKI.' in str(message) for message in get_messages(response.wsgi_request)) \ No newline at end of file diff --git a/trustpoint/pki/urls.py b/trustpoint/pki/urls.py index 563305542..5d5f3a0a1 100644 --- a/trustpoint/pki/urls.py +++ b/trustpoint/pki/urls.py @@ -136,6 +136,11 @@ issuing_cas.IssuingCaAddMethodSelectView.as_view(), name='issuing_cas-add-method_select', ), + path( + 'issuing-cas/add/autogen/', + issuing_cas.IssuingCaAddAutoGenView.as_view(), + name='issuing_cas-add-autogen', + ), path( 'issuing-cas/add/file-import/pkcs12', issuing_cas.IssuingCaAddFileImportPkcs12View.as_view(), diff --git a/trustpoint/pki/util/keys.py b/trustpoint/pki/util/keys.py index 8727716c6..9ebfd9eca 100644 --- a/trustpoint/pki/util/keys.py +++ b/trustpoint/pki/util/keys.py @@ -5,7 +5,6 @@ from __future__ import annotations -import logging from typing import TYPE_CHECKING, cast, get_args from cryptography.hazmat.primitives import hashes @@ -34,9 +33,6 @@ from pki.models.credential import CredentialModel from pki.models.domain import DomainModel -logger = logging.getLogger(__name__) - - class AutoGenPkiKeyAlgorithm(models.TextChoices): """The key algorithms supported by the AutoGenPKI.""" @@ -66,39 +62,6 @@ def to_public_key_info(self) -> PublicKeyInfo: raise ValueError(exc_msg) -def supported_auto_gen_pki_key_algorithms() -> tuple[AutoGenPkiKeyAlgorithm, ...]: - """Return AutoGenPKI algorithms supported by the active crypto backend.""" - from crypto.application.capabilities import get_active_backend_capability_report # noqa: PLC0415 - from crypto.domain.specs import MlDsaKeySpec, MlDsaVariant # noqa: PLC0415 - - report = get_active_backend_capability_report() - if not report.available: - logger.warning( - 'Could not determine supported AutoGenPKI algorithms for backend %r: %s', - report.backend_kind, - '; '.join(report.diagnostics) or 'backend unavailable', - ) - return () - - supported: list[AutoGenPkiKeyAlgorithm] = [] - if report.supports_rsa_key_size(2048): - supported.append(AutoGenPkiKeyAlgorithm.RSA2048) - if report.supports_rsa_key_size(4096): - supported.append(AutoGenPkiKeyAlgorithm.RSA4096) - if report.supports_ec_curve(ec.SECP256R1()): - supported.append(AutoGenPkiKeyAlgorithm.SECP256R1) - - if report.backend_kind == 'software': - if report.supports_key_spec(MlDsaKeySpec(variant=MlDsaVariant.MLDSA44)): - supported.append(AutoGenPkiKeyAlgorithm.MLDSA44) - if report.supports_key_spec(MlDsaKeySpec(variant=MlDsaVariant.MLDSA65)): - supported.append(AutoGenPkiKeyAlgorithm.MLDSA65) - if report.supports_key_spec(MlDsaKeySpec(variant=MlDsaVariant.MLDSA87)): - supported.append(AutoGenPkiKeyAlgorithm.MLDSA87) - - return tuple(supported) - - class KeyGenerator: """Utility class for generating private keys.""" diff --git a/trustpoint/pki/views/issuing_cas.py b/trustpoint/pki/views/issuing_cas.py index f8a36192d..d9a741491 100644 --- a/trustpoint/pki/views/issuing_cas.py +++ b/trustpoint/pki/views/issuing_cas.py @@ -38,7 +38,9 @@ from rest_framework.response import Response from management.models.audit_log import AuditLog +from management.models.security import SecurityConfig from pki.forms import ( + IssuingCaAddAutoGenForm, CertificateIssuanceForm, IssuingCaAddFileImportPkcs12Form, IssuingCaAddFileImportSeparateFilesForm, @@ -52,6 +54,7 @@ TruststoreAddForm, ) from pki.forms.issuing_cas import IssuingCaImportMixin +from pki.auto_gen_pki import AutoGenPki from pki.models import CaModel, CertificateModel, CredentialModel from pki.models.ca_rollover import CaRolloverModel, CaRolloverState from pki.models.cert_profile import CertificateProfileModel @@ -149,6 +152,26 @@ class IssuingCaAddMethodSelectView(IssuingCaContextMixin, FormView[IssuingCaAddM template_name = 'pki/issuing_cas/add/method_select.html' form_class = IssuingCaAddMethodSelectForm + def get_context_data(self, **kwargs: Any) -> dict[str, Any]: + """Add the independent AutoGenPKI form and its current availability.""" + context = super().get_context_data(**kwargs) + auto_gen_form = IssuingCaAddAutoGenForm() + security_config = SecurityConfig.objects.first() + active_auto_gen_pki = AutoGenPki.get_auto_gen_pki() + can_manage_cas = self.request.user.has_perm(AppPermissions.MANAGE_CAS) + auto_gen_allowed = bool(security_config and security_config.auto_gen_pki) + has_supported_key_types = any(value for value, _label in auto_gen_form.fields['key_type'].choices) + can_generate = auto_gen_allowed and active_auto_gen_pki is None and can_manage_cas and has_supported_key_types + if not can_generate: + auto_gen_form.fields['key_type'].widget.attrs['disabled'] = 'disabled' + context.update({ + 'auto_gen_pki_form': auto_gen_form, + 'auto_gen_pki_allowed': auto_gen_allowed, + 'active_auto_gen_pki': active_auto_gen_pki, + 'can_generate_auto_gen_pki': can_generate, + }) + return context + def form_valid(self, form: IssuingCaAddMethodSelectForm) -> HttpResponseRedirect: """Redirect to the next step based on the selected method.""" method_select = form.cleaned_data.get('method_select') @@ -167,6 +190,54 @@ def form_valid(self, form: IssuingCaAddMethodSelectForm) -> HttpResponseRedirect return HttpResponseRedirect(reverse_lazy('pki:issuing_cas-add-method_select')) +class IssuingCaAddAutoGenView(IssuingCaContextMixin, FormView[IssuingCaAddAutoGenForm]): + """Generate the local AutoGenPKI from the dedicated Add Issuing CA action.""" + + form_class = IssuingCaAddAutoGenForm + template_name = 'pki/issuing_cas/add/method_select.html' + success_url = reverse_lazy('pki:issuing_cas') + + def form_invalid(self, form: IssuingCaAddAutoGenForm) -> HttpResponseRedirect: + """Return to method selection with validation feedback.""" + for error in form.errors.values(): + messages.error(self.request, ' '.join(error)) + return redirect('pki:issuing_cas-add-method_select') + + def form_valid(self, form: IssuingCaAddAutoGenForm) -> HttpResponseRedirect: + """Enforce permissions and current security state before creating any CA.""" + if not self.request.user.has_perm(AppPermissions.MANAGE_CAS): + raise PermissionDenied + + security_config = SecurityConfig.objects.first() + if not security_config or not security_config.auto_gen_pki: + messages.error(self.request, _('Auto-generated PKI creation is disabled in the Security Settings.')) + return redirect('pki:issuing_cas-add-method_select') + + if AutoGenPki.get_auto_gen_pki() is not None: + messages.warning(self.request, _('An auto-generated PKI is already active.')) + return redirect('pki:issuing_cas-add-method_select') + + try: + issuing_ca = AutoGenPki.enable_auto_gen_pki(form.cleaned_data['key_type']) + except ValueError as error: + messages.error(self.request, str(error)) + return redirect('pki:issuing_cas-add-method_select') + + if issuing_ca is None: + messages.warning(self.request, _('An auto-generated PKI is already active.')) + return redirect('pki:issuing_cas-add-method_select') + + actor = self.request.user if self.request.user.is_authenticated else None + AuditLog.create_entry( + operation_type=AuditLog.OperationType.CA_CREATED, + target=issuing_ca, + target_display=f'CA: {issuing_ca.unique_name}', + actor=actor, + ) + messages.success(self.request, _('Successfully generated auto-generated PKI.')) + return redirect(self.success_url) + + class IssuingCaAddFileImportPkcs12View(IssuingCaContextMixin, FormView[IssuingCaAddFileImportPkcs12Form]): """View to import an Issuing CA from a PKCS12 file.""" diff --git a/trustpoint/templates/management/backend_configuration.html b/trustpoint/templates/management/backend_configuration.html index 33ad80fa7..b829c0d0b 100644 --- a/trustpoint/templates/management/backend_configuration.html +++ b/trustpoint/templates/management/backend_configuration.html @@ -342,9 +342,9 @@

{% trans 'AutoGenPKI Choices' %}

- {% if supported_auto_gen_pki_algorithms %} + {% if supported_auto_gen_pki_key_types %} - {{ supported_auto_gen_pki_algorithms|length }} + {{ supported_auto_gen_pki_key_types|length }} {% endif %} @@ -353,12 +353,12 @@

- {% if supported_auto_gen_pki_algorithms %} + {% if supported_auto_gen_pki_key_types %}
- {% for algorithm in supported_auto_gen_pki_algorithms %} + {% for key_type, key_type_label in supported_auto_gen_pki_key_types %} - {{ algorithm.label }} + {{ key_type_label }} {% endfor %}
@@ -366,7 +366,7 @@

{% else %}
- {% trans 'Auto-generated PKI cannot be enabled until the active backend supports at least one allowed key algorithm.' %} + {% trans 'Auto-generated PKI cannot be generated until the active backend supports at least one key type.' %}
{% endif %} diff --git a/trustpoint/templates/management/includes/security_configuration.html b/trustpoint/templates/management/includes/security_configuration.html index 61abc2fc7..838b7aa7f 100644 --- a/trustpoint/templates/management/includes/security_configuration.html +++ b/trustpoint/templates/management/includes/security_configuration.html @@ -186,7 +186,6 @@

{# Footer #} {# ============================================================ #} diff --git a/trustpoint/templates/pki/issuing_cas/add/method_select.html b/trustpoint/templates/pki/issuing_cas/add/method_select.html index 0d51792e1..f7e8997b2 100644 --- a/trustpoint/templates/pki/issuing_cas/add/method_select.html +++ b/trustpoint/templates/pki/issuing_cas/add/method_select.html @@ -301,4 +301,43 @@

+ +
+
+

{% trans 'Auto-generated PKI' %}

+

+ {% trans 'Generate a local Root CA, Issuing CA and Domain managed by Trustpoint.' %} +

+
+
+ {% if active_auto_gen_pki %} +
+ {% trans 'An auto-generated PKI is already active.' %} + + {% trans 'View the active Issuing CA' %} + +
+ {% elif not auto_gen_pki_allowed %} +
+ {% trans 'Auto-generated PKI creation is disabled in the Security Settings.' %} + + {% trans 'Security Settings' %} + +
+ {% endif %} + +
+ {% csrf_token %} +
+ + {{ auto_gen_pki_form.key_type }} +
+ +
+
+
{% endblock content %} \ No newline at end of file From 20f5c12c2d693b2f3d305104db5e6e4d5f024dc4 Mon Sep 17 00:00:00 2001 From: florianhandke Date: Tue, 6 Oct 2026 11:56:16 +0200 Subject: [PATCH 2/2] fix mypy --- .github-assets/trustpoint_banner.png | Bin 24383 -> 0 bytes .github-assets/trustpoint_github_banner.svg | 1 + README.md | 2 +- docs/source/_static/trustpoint_banner.png | Bin 24383 -> 0 bytes .../_static/trustpoint_github_banner.svg | 1 + docs/source/cra/CONTROLS.md | 2 +- docs/source/cra/CRA_COMPLIANCE.md | 2 +- docs/source/cra/RISK_REGISTER.md | 2 +- docs/source/cra/THREAT_MODEL.md | 2 +- docs/source/index.rst | 2 +- trustpoint/management/forms.py | 4 ++-- trustpoint/pki/auto_gen_pki.py | 3 ++- trustpoint/pki/forms/__init__.py | 2 +- trustpoint/pki/forms/issuing_cas.py | 10 +++++++--- trustpoint/pki/views/issuing_cas.py | 17 +++++++++++------ 15 files changed, 31 insertions(+), 19 deletions(-) delete mode 100644 .github-assets/trustpoint_banner.png create mode 100644 .github-assets/trustpoint_github_banner.svg delete mode 100644 docs/source/_static/trustpoint_banner.png create mode 100644 docs/source/_static/trustpoint_github_banner.svg diff --git a/.github-assets/trustpoint_banner.png b/.github-assets/trustpoint_banner.png deleted file mode 100644 index c1d9b9a7969cc161a9eaa1463635346053366786..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24383 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 \ No newline at end of file diff --git a/README.md b/README.md index 67a48886a..c11df6b78 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![Trustpoint](.github-assets/trustpoint_banner.png) +![Trustpoint](.github-assets/trustpoint_github_banner.svg)
diff --git a/docs/source/_static/trustpoint_banner.png b/docs/source/_static/trustpoint_banner.png deleted file mode 100644 index c1d9b9a7969cc161a9eaa1463635346053366786..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24383 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 \ No newline at end of file diff --git a/docs/source/cra/CONTROLS.md b/docs/source/cra/CONTROLS.md index 197b5f4ea..73e81b154 100644 --- a/docs/source/cra/CONTROLS.md +++ b/docs/source/cra/CONTROLS.md @@ -1,5 +1,5 @@

- Trustpoint Logo + Trustpoint Logo

# Security Controls diff --git a/docs/source/cra/CRA_COMPLIANCE.md b/docs/source/cra/CRA_COMPLIANCE.md index 553cd2cc2..8610489f5 100644 --- a/docs/source/cra/CRA_COMPLIANCE.md +++ b/docs/source/cra/CRA_COMPLIANCE.md @@ -1,5 +1,5 @@

- Trustpoint Logo + Trustpoint Logo

# CRA Conformity Assessment diff --git a/docs/source/cra/RISK_REGISTER.md b/docs/source/cra/RISK_REGISTER.md index 1dd17f10a..22e0d0b22 100644 --- a/docs/source/cra/RISK_REGISTER.md +++ b/docs/source/cra/RISK_REGISTER.md @@ -1,5 +1,5 @@

- Trustpoint Logo + Trustpoint Logo

# Risk Register diff --git a/docs/source/cra/THREAT_MODEL.md b/docs/source/cra/THREAT_MODEL.md index bed02780d..552982c27 100644 --- a/docs/source/cra/THREAT_MODEL.md +++ b/docs/source/cra/THREAT_MODEL.md @@ -1,5 +1,5 @@

- Trustpoint Logo + Trustpoint Logo

# Threat Model diff --git a/docs/source/index.rst b/docs/source/index.rst index 1c283a7c7..4f9590fd4 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -3,7 +3,7 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -.. image:: /_static/trustpoint_banner.png +.. image:: /_static/trustpoint_github_banner.svg :align: center ====================================== diff --git a/trustpoint/management/forms.py b/trustpoint/management/forms.py index 19517cfdf..59d0a81ae 100644 --- a/trustpoint/management/forms.py +++ b/trustpoint/management/forms.py @@ -426,11 +426,11 @@ def clean(self) -> dict[str, Any]: self._validate_mode_constraints(cleaned, str(mode)) selected_mode = str(mode) - mode_defaults = SecurityConfig._MODE_DEFAULTS.get(selected_mode, {}) # noqa: SLF001 + mode_defaults = SecurityConfig._MODE_DEFAULTS.get(selected_mode) # noqa: SLF001 if self.instance and selected_mode == self.instance.security_mode: auto_gen_pki_allowed = self.instance.allow_auto_gen_pki else: - auto_gen_pki_allowed = mode_defaults.get('allow_auto_gen_pki', False) + auto_gen_pki_allowed = mode_defaults['allow_auto_gen_pki'] if mode_defaults else False if cleaned.get('auto_gen_pki') and not auto_gen_pki_allowed: self.add_error('auto_gen_pki', 'Cannot enable auto-generated PKI when it is not permitted.') diff --git a/trustpoint/pki/auto_gen_pki.py b/trustpoint/pki/auto_gen_pki.py index 7a77edadb..71be886da 100644 --- a/trustpoint/pki/auto_gen_pki.py +++ b/trustpoint/pki/auto_gen_pki.py @@ -131,7 +131,8 @@ def enable_auto_gen_pki( """Enables the auto-generated PKI.""" key_type = key_type or key_alg if key_type is None: - raise ValueError('An AutoGenPKI key type is required.') + msg = 'An AutoGenPKI key type is required.' + raise ValueError(msg) key_type = cls._normalize_key_type(key_type) key_identifier = cls._key_type_identifier(key_type) with cls._lock: diff --git a/trustpoint/pki/forms/__init__.py b/trustpoint/pki/forms/__init__.py index 325a4cdeb..978d3d681 100644 --- a/trustpoint/pki/forms/__init__.py +++ b/trustpoint/pki/forms/__init__.py @@ -30,13 +30,13 @@ from .truststores import TruststoreAddForm, TruststoreDownloadForm __all__ = [ - 'IssuingCaAddAutoGenForm', 'CertProfileConfigForm', 'CertificateDownloadForm', 'CertificateIssuanceForm', 'CsrIssuanceForm', 'DevIdAddMethodSelectForm', 'DevIdRegistrationForm', + 'IssuingCaAddAutoGenForm', 'IssuingCaAddFileImportPkcs12Form', 'IssuingCaAddFileImportSeparateFilesForm', 'IssuingCaAddMethodSelectForm', diff --git a/trustpoint/pki/forms/issuing_cas.py b/trustpoint/pki/forms/issuing_cas.py index 9e9775cc9..256a43141 100644 --- a/trustpoint/pki/forms/issuing_cas.py +++ b/trustpoint/pki/forms/issuing_cas.py @@ -308,6 +308,8 @@ class IssuingCaAddMethodSelectForm(forms.Form): class IssuingCaAddAutoGenForm(forms.Form): """Form for generating the local auto-generated PKI.""" + has_supported_key_types: bool + key_type = forms.ChoiceField( label=_('Key Type'), choices=KEY_TYPE_CHOICES, @@ -320,13 +322,15 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: """Use only key types supported by the active crypto backend.""" super().__init__(*args, **kwargs) choices = supported_key_type_choices() - self.fields['key_type'].choices = choices or [('', _('No supported backend algorithms available'))] + self.has_supported_key_types = bool(choices) + key_type_field = cast('forms.ChoiceField', self.fields['key_type']) + key_type_field.choices = choices or [('', _('No supported backend algorithms available'))] if not choices: - self.fields['key_type'].widget.attrs['disabled'] = 'disabled' + key_type_field.widget.attrs['disabled'] = 'disabled' def clean_key_type(self) -> str: """Reject key types that the active backend cannot generate.""" - key_type = self.cleaned_data['key_type'] + key_type = cast('str', self.cleaned_data['key_type']) supported_types = {value for value, _label in supported_key_type_choices()} if key_type not in supported_types: raise ValidationError(_('The active crypto backend does not support this key type.')) diff --git a/trustpoint/pki/views/issuing_cas.py b/trustpoint/pki/views/issuing_cas.py index d9a741491..ea49f12f1 100644 --- a/trustpoint/pki/views/issuing_cas.py +++ b/trustpoint/pki/views/issuing_cas.py @@ -39,9 +39,10 @@ from management.models.audit_log import AuditLog from management.models.security import SecurityConfig +from pki.auto_gen_pki import AutoGenPki from pki.forms import ( - IssuingCaAddAutoGenForm, CertificateIssuanceForm, + IssuingCaAddAutoGenForm, IssuingCaAddFileImportPkcs12Form, IssuingCaAddFileImportSeparateFilesForm, IssuingCaAddMethodSelectForm, @@ -54,7 +55,6 @@ TruststoreAddForm, ) from pki.forms.issuing_cas import IssuingCaImportMixin -from pki.auto_gen_pki import AutoGenPki from pki.models import CaModel, CertificateModel, CredentialModel from pki.models.ca_rollover import CaRolloverModel, CaRolloverState from pki.models.cert_profile import CertificateProfileModel @@ -160,10 +160,15 @@ def get_context_data(self, **kwargs: Any) -> dict[str, Any]: active_auto_gen_pki = AutoGenPki.get_auto_gen_pki() can_manage_cas = self.request.user.has_perm(AppPermissions.MANAGE_CAS) auto_gen_allowed = bool(security_config and security_config.auto_gen_pki) - has_supported_key_types = any(value for value, _label in auto_gen_form.fields['key_type'].choices) - can_generate = auto_gen_allowed and active_auto_gen_pki is None and can_manage_cas and has_supported_key_types + key_type_field = cast('ChoiceField', auto_gen_form.fields['key_type']) + can_generate = ( + auto_gen_allowed + and active_auto_gen_pki is None + and can_manage_cas + and auto_gen_form.has_supported_key_types + ) if not can_generate: - auto_gen_form.fields['key_type'].widget.attrs['disabled'] = 'disabled' + key_type_field.widget.attrs['disabled'] = 'disabled' context.update({ 'auto_gen_pki_form': auto_gen_form, 'auto_gen_pki_allowed': auto_gen_allowed, @@ -200,7 +205,7 @@ class IssuingCaAddAutoGenView(IssuingCaContextMixin, FormView[IssuingCaAddAutoGe def form_invalid(self, form: IssuingCaAddAutoGenForm) -> HttpResponseRedirect: """Return to method selection with validation feedback.""" for error in form.errors.values(): - messages.error(self.request, ' '.join(error)) + messages.error(self.request, ' '.join(str(message) for message in error)) return redirect('pki:issuing_cas-add-method_select') def form_valid(self, form: IssuingCaAddAutoGenForm) -> HttpResponseRedirect: