Source code for management.models.security

"""Security Configuration Model."""
from __future__ import annotations

import json
from typing import TYPE_CHECKING, ClassVar, TypedDict

from django.db import models
from django.utils.translation import gettext_lazy as _
from trustpoint_core.oid import AlgorithmIdentifier, HashAlgorithm, NamedCurve, PublicKeyAlgorithmOid

from onboarding.enums import NoOnboardingPkiProtocol, OnboardingProtocol
from pki.util.keys import AutoGenPkiKeyAlgorithm

if TYPE_CHECKING:
    from devices.models import DeviceModel
    from pki.models.ca import CaModel


class _SecurityModeDefaults(TypedDict):
    """Typed structure for per-mode security defaults."""

    rsa_minimum_key_size: int | None
    not_permitted_ecc_curve_oids: list[str]
    not_permitted_signature_algorithm_oids: list[str]
    max_cert_validity_days: int | None
    max_crl_validity_days: int | None
    allow_ca_issuance: bool
    allow_auto_gen_pki: bool
    allow_self_signed_ca: bool
    require_physical_hsm: bool
    permitted_no_onboarding_pki_protocols: list[int]
    permitted_onboarding_protocols: list[int]


[docs] class SecurityConfig(models.Model): """Security Configuration model. Stores the active security level together with all security thresholds for that level: * :attr:`rsa_minimum_key_size` — minimum acceptable RSA key size in bits (``0`` = any RSA key size allowed, ``None`` = RSA not allowed). * :attr:`not_permitted_ecc_curve_oids` — JSON list of :class:`trustpoint_core.oid.NamedCurve` OID strings that are not permitted at the current level. * :attr:`not_permitted_signature_algorithm_oids` — JSON list of :class:`trustpoint_core.oid.HashAlgorithm` OID strings that are not permitted. * :attr:`max_cert_validity_days` — maximum certificate validity in days (``None`` = no limit). * :attr:`max_crl_validity_days` — maximum CRL validity in days (``None`` = no limit). * :attr:`allow_ca_issuance` — whether BasicConstraints ca=True is permitted in issued certs. * :attr:`allow_auto_gen_pki` — whether auto-generated PKI may be enabled. * :attr:`allow_self_signed_ca` — whether self-signed CAs with credentials are allowed. * :attr:`require_physical_hsm` — whether key storage must be a physical HSM. * :attr:`permitted_no_onboarding_pki_protocols` — JSON list of allowed :class:`onboarding.models.NoOnboardingPkiProtocol` integer values. * :attr:`permitted_onboarding_protocols` — JSON list of allowed :class:`onboarding.models.OnboardingProtocol` integer values. Calling :meth:`apply_security_settings` applies the mode defaults onto this instance. """
[docs] class SecurityModeChoices(models.TextChoices): """Types of security modes."""
[docs] LAB = '0', _('Lab / Development')
[docs] BROWNFIELD = '1', _('Brownfield Compatible')
[docs] INDUSTRIAL = '2', _('Industrial Standard')
[docs] HARDENED = '3', _('Hardened Production')
[docs] CRITICAL = '4', _('Critical Infrastructure')
# ------------------------------------------------------------------ # Choices derived from trustpoint_core.oid enums # ------------------------------------------------------------------
[docs] class NamedCurveChoices(models.TextChoices): """Selectable ECC curve OIDs sourced from :class:`trustpoint_core.oid.NamedCurve`."""
[docs] SECP192R1 = NamedCurve.SECP192R1.dotted_string, _('SECP192R1')
[docs] SECP224R1 = NamedCurve.SECP224R1.dotted_string, _('SECP224R1')
[docs] SECP256K1 = NamedCurve.SECP256K1.dotted_string, _('SECP256K1')
[docs] SECP256R1 = NamedCurve.SECP256R1.dotted_string, _('SECP256R1')
[docs] SECP384R1 = NamedCurve.SECP384R1.dotted_string, _('SECP384R1')
[docs] SECP521R1 = NamedCurve.SECP521R1.dotted_string, _('SECP521R1')
[docs] BRAINPOOLP256R1 = NamedCurve.BRAINPOOLP256R1.dotted_string, _('BrainpoolP256R1')
[docs] BRAINPOOLP384R1 = NamedCurve.BRAINPOOLP384R1.dotted_string, _('BrainpoolP384R1')
[docs] BRAINPOOLP512R1 = NamedCurve.BRAINPOOLP512R1.dotted_string, _('BrainpoolP512R1')
[docs] class HashAlgorithmChoices(models.TextChoices): """Selectable hash/signature algorithm OIDs from :class:`trustpoint_core.oid.HashAlgorithm`."""
[docs] MD5 = HashAlgorithm.MD5.dotted_string, _('MD5')
[docs] SHA1 = HashAlgorithm.SHA1.dotted_string, _('SHA-1')
[docs] SHA224 = HashAlgorithm.SHA224.dotted_string, _('SHA-224')
[docs] SHA256 = HashAlgorithm.SHA256.dotted_string, _('SHA-256')
[docs] SHA384 = HashAlgorithm.SHA384.dotted_string, _('SHA-384')
[docs] SHA512 = HashAlgorithm.SHA512.dotted_string, _('SHA-512')
# ------------------------------------------------------------------ # Model fields # ------------------------------------------------------------------
[docs] security_mode = models.CharField( max_length=6, choices=SecurityModeChoices, default=SecurityModeChoices.BROWNFIELD, )
[docs] auto_gen_pki = models.BooleanField(default=False)
[docs] auto_gen_pki_key_algorithm = models.CharField( max_length=24, choices=AutoGenPkiKeyAlgorithm, default=AutoGenPkiKeyAlgorithm.RSA2048, )
# -- Key constraints --------------------------------------------------
[docs] rsa_minimum_key_size = models.PositiveIntegerField( null=True, blank=True, default=2048, help_text=_( 'Minimum RSA key size in bits that certificates must meet. ' 'Set to null to disallow RSA entirely.' ), )
[docs] not_permitted_ecc_curve_oids = models.JSONField( default=list, blank=True, help_text=_( 'JSON list of ECC curve OIDs (from trustpoint_core.oid.NamedCurve) ' 'not permitted at the current security level.' ), )
[docs] not_permitted_signature_algorithm_oids = models.JSONField( default=list, blank=True, help_text=_( 'JSON list of hash algorithm OIDs (from trustpoint_core.oid.HashAlgorithm) ' 'not permitted at the current security level.' ), )
# -- Certificate / CRL validity ---------------------------------------
[docs] max_cert_validity_days = models.PositiveIntegerField( null=True, blank=True, default=None, help_text=_( 'Maximum certificate validity period in days. ' 'Set to null for no limit.' ), )
[docs] max_crl_validity_days = models.PositiveIntegerField( null=True, blank=True, default=None, help_text=_( 'Maximum CRL validity period in days. ' 'Set to null for no limit.' ), )
# -- Certificate policy flags -----------------------------------------
[docs] allow_ca_issuance = models.BooleanField( default=False, help_text=_('Allow issuance of certificates with BasicConstraints ca=True.'), )
[docs] allow_auto_gen_pki = models.BooleanField( default=False, help_text=_('Allow enabling the auto-generated PKI feature.'), )
[docs] allow_self_signed_ca = models.BooleanField( default=False, help_text=_('Allow self-signed CAs to be imported with credentials.'), )
# -- Infrastructure constraints ----------------------------------------
[docs] require_physical_hsm = models.BooleanField( default=False, help_text=_('Require key storage to use a physical HSM (KeyStorageConfig.StorageType.PHYSICAL_HSM).'), )
# -- Protocol allow-lists (stored as JSON lists of integer values) -----
[docs] permitted_no_onboarding_pki_protocols = models.JSONField( default=list, blank=True, help_text=_( 'JSON list of allowed NoOnboardingPkiProtocol integer values ' '(bitmask flags: CMP_SHARED_SECRET=1, EST_USERNAME_PASSWORD=4, MANUAL=16).' ), )
[docs] permitted_onboarding_protocols = models.JSONField( default=list, blank=True, help_text=_( 'JSON list of allowed OnboardingProtocol integer values ' '(MANUAL=0, CMP_IDEVID=1, CMP_SHARED_SECRET=2, EST_IDEVID=3, ' 'EST_USERNAME_PASSWORD=4, AOKI=5, BRSKI=6, OPC_GDS_PUSH=7).' ), )
# ------------------------------------------------------------------ # Default configurations keyed by mode # ------------------------------------------------------------------ #: All OnboardingProtocol values _ALL_ONBOARDING_PROTOCOLS: ClassVar[list[int]] = [0, 1, 2, 3, 4, 5, 6, 7] #: All OnboardingProtocol values except MANUAL (0) _ONBOARDING_PROTOCOLS_NO_MANUAL: ClassVar[list[int]] = [1, 2, 3, 4, 5, 6, 7] _MODE_DEFAULTS: ClassVar[dict[str, _SecurityModeDefaults]] = { # ---------------------------------------------------------------- # Lab / Development # ---------------------------------------------------------------- SecurityModeChoices.LAB: { 'rsa_minimum_key_size': 0, # any RSA key size allowed 'not_permitted_ecc_curve_oids': [], 'not_permitted_signature_algorithm_oids': [], 'max_cert_validity_days': None, # no limit 'max_crl_validity_days': None, # no limit 'allow_ca_issuance': True, 'allow_auto_gen_pki': True, 'allow_self_signed_ca': True, 'require_physical_hsm': False, 'permitted_no_onboarding_pki_protocols': [1, 4, 16], # CMP_SHARED_SECRET, EST_USERNAME_PASSWORD, MANUAL 'permitted_onboarding_protocols': _ALL_ONBOARDING_PROTOCOLS, }, # ---------------------------------------------------------------- # Brownfield Compatible # ---------------------------------------------------------------- SecurityModeChoices.BROWNFIELD: { 'rsa_minimum_key_size': 1024, 'not_permitted_ecc_curve_oids': [], 'not_permitted_signature_algorithm_oids': [], 'max_cert_validity_days': 1825, # 5 years 'max_crl_validity_days': 365, 'allow_ca_issuance': False, 'allow_auto_gen_pki': True, 'allow_self_signed_ca': True, 'require_physical_hsm': False, 'permitted_no_onboarding_pki_protocols': [1, 4, 16], # CMP_SHARED_SECRET, EST_USERNAME_PASSWORD, MANUAL 'permitted_onboarding_protocols': _ALL_ONBOARDING_PROTOCOLS, }, # ---------------------------------------------------------------- # Industrial Standard # ---------------------------------------------------------------- SecurityModeChoices.INDUSTRIAL: { 'rsa_minimum_key_size': 3072, 'not_permitted_ecc_curve_oids': [ NamedCurveChoices.SECP192R1, NamedCurveChoices.SECP224R1, ], 'not_permitted_signature_algorithm_oids': [ HashAlgorithmChoices.MD5, HashAlgorithmChoices.SHA1, ], 'max_cert_validity_days': 365, 'max_crl_validity_days': 180, 'allow_ca_issuance': False, 'allow_auto_gen_pki': False, 'allow_self_signed_ca': False, 'require_physical_hsm': False, 'permitted_no_onboarding_pki_protocols': [1, 4, 16], # CMP_SHARED_SECRET, EST_USERNAME_PASSWORD, MANUAL 'permitted_onboarding_protocols': _ALL_ONBOARDING_PROTOCOLS, }, # ---------------------------------------------------------------- # Hardened Production # ---------------------------------------------------------------- SecurityModeChoices.HARDENED: { 'rsa_minimum_key_size': 4096, 'not_permitted_ecc_curve_oids': [ NamedCurveChoices.SECP192R1, NamedCurveChoices.SECP224R1, NamedCurveChoices.SECP256K1, ], 'not_permitted_signature_algorithm_oids': [ HashAlgorithmChoices.MD5, HashAlgorithmChoices.SHA1, HashAlgorithmChoices.SHA224, ], 'max_cert_validity_days': 365, 'max_crl_validity_days': 90, 'allow_ca_issuance': False, 'allow_auto_gen_pki': False, 'allow_self_signed_ca': False, 'require_physical_hsm': False, 'permitted_no_onboarding_pki_protocols': [1, 4], # CMP_SHARED_SECRET, EST_USERNAME_PASSWORD 'permitted_onboarding_protocols': _ONBOARDING_PROTOCOLS_NO_MANUAL, }, # ---------------------------------------------------------------- # Critical Infrastructure # ---------------------------------------------------------------- SecurityModeChoices.CRITICAL: { 'rsa_minimum_key_size': None, 'not_permitted_ecc_curve_oids': [ NamedCurveChoices.SECP192R1, NamedCurveChoices.SECP224R1, NamedCurveChoices.SECP256K1, NamedCurveChoices.SECP256R1, NamedCurveChoices.BRAINPOOLP256R1, ], 'not_permitted_signature_algorithm_oids': [ HashAlgorithmChoices.MD5, HashAlgorithmChoices.SHA1, HashAlgorithmChoices.SHA224, HashAlgorithmChoices.SHA256, ], 'max_cert_validity_days': 180, 'max_crl_validity_days': 90, 'allow_ca_issuance': False, 'allow_auto_gen_pki': False, 'allow_self_signed_ca': False, 'require_physical_hsm': True, 'permitted_no_onboarding_pki_protocols': [1, 4], # CMP_SHARED_SECRET, EST_USERNAME_PASSWORD 'permitted_onboarding_protocols': _ONBOARDING_PROTOCOLS_NO_MANUAL, }, } def __str__(self) -> str: """Output as string.""" return f'{self.security_mode}'
[docs] def apply_security_settings(self) -> None: """Reset all thresholds to the defaults for the current security mode.""" if not self.security_mode: return defaults = self._MODE_DEFAULTS.get(self.security_mode) if defaults is None: msg = f'No defaults defined for security mode: {self.security_mode}' raise ValueError(msg) self.rsa_minimum_key_size = defaults['rsa_minimum_key_size'] self.not_permitted_ecc_curve_oids = list(defaults['not_permitted_ecc_curve_oids']) self.not_permitted_signature_algorithm_oids = list(defaults['not_permitted_signature_algorithm_oids']) self.max_cert_validity_days = defaults['max_cert_validity_days'] self.max_crl_validity_days = defaults['max_crl_validity_days'] self.allow_ca_issuance = defaults['allow_ca_issuance'] self.allow_auto_gen_pki = defaults['allow_auto_gen_pki'] self.allow_self_signed_ca = defaults['allow_self_signed_ca'] self.require_physical_hsm = defaults['require_physical_hsm'] self.permitted_no_onboarding_pki_protocols = list(defaults['permitted_no_onboarding_pki_protocols']) self.permitted_onboarding_protocols = list(defaults['permitted_onboarding_protocols']) self.save(update_fields=[ 'rsa_minimum_key_size', 'not_permitted_ecc_curve_oids', 'not_permitted_signature_algorithm_oids', 'max_cert_validity_days', 'max_crl_validity_days', 'allow_ca_issuance', 'allow_auto_gen_pki', 'allow_self_signed_ca', 'require_physical_hsm', 'permitted_no_onboarding_pki_protocols', 'permitted_onboarding_protocols', ])
@classmethod
[docs] def get_settings_preview_json(cls) -> str: """Return a JSON string of each mode's display-friendly threshold values for the settings JS.""" ecc_labels = dict(cls.NamedCurveChoices.choices) sig_labels = dict(cls.HashAlgorithmChoices.choices) no_onboarding_labels: dict[int, str] = {c.value: str(c.label) for c in NoOnboardingPkiProtocol} onboarding_labels: dict[int, str] = {c.value: str(c.label) for c in OnboardingProtocol} preview: dict[str, dict[str, object]] = {} for mode, defaults in cls._MODE_DEFAULTS.items(): preview[mode] = { 'rsa_minimum_key_size': defaults['rsa_minimum_key_size'], 'not_permitted_ecc_curves': [ str(ecc_labels.get(oid, oid)) for oid in defaults['not_permitted_ecc_curve_oids'] ], 'not_permitted_signature_algorithms': [ str(sig_labels.get(oid, oid)) for oid in defaults['not_permitted_signature_algorithm_oids'] ], 'max_cert_validity_days': defaults['max_cert_validity_days'], 'max_crl_validity_days': defaults['max_crl_validity_days'], 'allow_ca_issuance': defaults['allow_ca_issuance'], 'allow_auto_gen_pki': defaults['allow_auto_gen_pki'], 'allow_self_signed_ca': defaults['allow_self_signed_ca'], 'require_physical_hsm': defaults['require_physical_hsm'], 'permitted_no_onboarding_pki_protocols': [ no_onboarding_labels.get(v, str(v)) for v in defaults['permitted_no_onboarding_pki_protocols'] ], 'permitted_onboarding_protocols': [ onboarding_labels.get(v, str(v)) for v in defaults['permitted_onboarding_protocols'] ], } return json.dumps(preview)
[docs] def check_mode_transition(self, target_mode: str) -> list[str]: """Check whether the existing data satisfies all requirements of *target_mode*.""" defaults = self._MODE_DEFAULTS.get(target_mode) if defaults is None: msg = f'No defaults defined for security mode: {target_mode}' raise ValueError(msg) mode_label = str(self.SecurityModeChoices(target_mode).label) violations: list[str] = [] from devices.models import DeviceModel # noqa: PLC0415 from pki.models.ca import CaModel # noqa: PLC0415 violations.extend(self._check_hsm(defaults, mode_label)) violations.extend(self._check_auto_gen_pki(defaults, mode_label)) violations.extend(self._check_self_signed_cas(defaults, mode_label, CaModel)) violations.extend(self._check_rsa_key_size(defaults, mode_label, CaModel)) violations.extend(self._check_ecc_curves(defaults, mode_label, CaModel)) violations.extend(self._check_signature_algorithms(defaults, mode_label, CaModel)) violations.extend(self._check_crl_validity(defaults, mode_label, CaModel)) violations.extend(self._check_no_onboarding_protocols(defaults, mode_label, CaModel, DeviceModel)) violations.extend(self._check_onboarding_protocols(defaults, mode_label, DeviceModel)) return violations
# ------------------------------------------------------------------ # Private helpers — one method per check group # ------------------------------------------------------------------ def _check_hsm(self, defaults: _SecurityModeDefaults, mode_label: str) -> list[str]: """Return violations for the require_physical_hsm constraint.""" if not defaults['require_physical_hsm']: return [] from management.models.key_storage import KeyStorageConfig # noqa: PLC0415 try: ks = KeyStorageConfig.get_config() if ks.storage_type != KeyStorageConfig.StorageType.PHYSICAL_HSM: return [str(_( 'Key storage is "%(storage_type)s" but %(mode)s requires a Physical HSM.' ) % {'storage_type': ks.get_storage_type_display(), 'mode': mode_label})] except KeyStorageConfig.DoesNotExist: return [str(_( '%(mode)s requires a Physical HSM but no key storage configuration exists.' ) % {'mode': mode_label})] return [] def _check_auto_gen_pki(self, defaults: _SecurityModeDefaults, mode_label: str) -> list[str]: """Return a violation if auto-generated PKI is enabled but not permitted in the target mode.""" if not defaults['allow_auto_gen_pki'] and self.auto_gen_pki: return [str(_( 'Auto-generated PKI is currently enabled but is not permitted in %(mode)s.' ) % {'mode': mode_label})] return [] @staticmethod def _check_self_signed_cas( defaults: _SecurityModeDefaults, mode_label: str, ca_model: type[CaModel], ) -> list[str]: """Return violations for self-signed issuing CAs when the target mode forbids them.""" if defaults['allow_self_signed_ca']: return [] names = ca_model.objects.filter( credential__certificate__is_self_signed=True ).values_list('unique_name', flat=True) return [ str(_( 'Issuing CA "%(ca)s" has a self-signed certificate, which is not permitted in %(mode)s.' ) % {'ca': n, 'mode': mode_label}) for n in names ] @staticmethod def _check_rsa_key_size( defaults: _SecurityModeDefaults, mode_label: str, ca_model: type[CaModel], ) -> list[str]: """Return violations for CA RSA keys below the minimum (or any RSA if banned).""" rsa_oid = PublicKeyAlgorithmOid.RSA.dotted_string min_rsa: int | None = defaults['rsa_minimum_key_size'] if min_rsa is None: names = ca_model.objects.filter( credential__certificate__spki_algorithm_oid=rsa_oid ).values_list('unique_name', flat=True) return [ str(_( 'Issuing CA "%(ca)s" uses an RSA key, which is not permitted in %(mode)s.' ) % {'ca': n, 'mode': mode_label}) for n in names ] if min_rsa == 0: # Any RSA key size is acceptable — nothing to check. return [] rows = ca_model.objects.filter( credential__certificate__spki_algorithm_oid=rsa_oid, credential__certificate__spki_key_size__lt=min_rsa, ).values_list('unique_name', 'credential__certificate__spki_key_size') return [ str(_( 'Issuing CA "%(ca)s" has an RSA key of %(size)d bits, below the minimum of %(min)d bits ' 'required by %(mode)s.' ) % {'ca': n, 'size': sz, 'min': min_rsa, 'mode': mode_label}) for n, sz in rows ] @staticmethod def _check_ecc_curves( defaults: _SecurityModeDefaults, mode_label: str, ca_model: type[CaModel], ) -> list[str]: """Return violations for CA certificates using a blocked ECC curve.""" blocked: list[str] = defaults['not_permitted_ecc_curve_oids'] if not blocked: return [] ecc_oid = PublicKeyAlgorithmOid.ECC.dotted_string rows = ca_model.objects.filter( credential__certificate__spki_algorithm_oid=ecc_oid, credential__certificate__spki_ec_curve_oid__in=blocked, ).values_list('unique_name', 'credential__certificate__spki_ec_curve_oid') curve_labels = dict(SecurityConfig.NamedCurveChoices.choices) return [ str(_( 'Issuing CA "%(ca)s" uses ECC curve %(curve)s, which is not permitted in %(mode)s.' ) % {'ca': n, 'curve': curve_labels.get(oid, oid), 'mode': mode_label}) for n, oid in rows ] @staticmethod def _check_signature_algorithms( defaults: _SecurityModeDefaults, mode_label: str, ca_model: type[CaModel], ) -> list[str]: """Return violations for CA certificates signed with a blocked hash algorithm.""" blocked_hash_oids: list[str] = defaults['not_permitted_signature_algorithm_oids'] if not blocked_hash_oids: return [] # Map AlgorithmIdentifier OIDs (stored in CertificateModel) to the hash OIDs blocked_sig_oids = [ alg_id.value.dotted_string for alg_id in AlgorithmIdentifier if alg_id.hash_algorithm is not None and alg_id.hash_algorithm.dotted_string in blocked_hash_oids ] if not blocked_sig_oids: return [] rows = ca_model.objects.filter( credential__certificate__signature_algorithm_oid__in=blocked_sig_oids, ).values_list('unique_name', 'credential__certificate__signature_algorithm_oid') hash_labels = dict(SecurityConfig.HashAlgorithmChoices.choices) violations: list[str] = [] for ca_name, sig_oid in rows: try: hash_algo = AlgorithmIdentifier.from_dotted_string(sig_oid).hash_algorithm label = hash_labels.get(hash_algo.dotted_string if hash_algo else '', sig_oid) except ValueError: label = sig_oid violations.append(str(_( 'Issuing CA "%(ca)s" uses signature algorithm with hash %(hash)s, ' 'which is not permitted in %(mode)s.' ) % {'ca': ca_name, 'hash': label, 'mode': mode_label})) return violations @staticmethod def _check_crl_validity( defaults: _SecurityModeDefaults, mode_label: str, ca_model: type[CaModel], ) -> list[str]: """Return violations for CAs whose CRL validity exceeds the target mode maximum.""" max_days: int | None = defaults['max_crl_validity_days'] if max_days is None: return [] max_hours = float(max_days) * 24 rows = ca_model.objects.filter( crl_validity_hours__gt=max_hours ).values_list('unique_name', 'crl_validity_hours') return [ str(_( 'Issuing CA "%(ca)s" has a CRL validity of %(hours).2f hours (%(days).1f days), ' 'exceeding the maximum of %(max_days)d days in %(mode)s.' ) % {'ca': n, 'hours': h, 'days': h / 24, 'max_days': max_days, 'mode': mode_label}) for n, h in rows ] @staticmethod def _check_no_onboarding_protocols( defaults: _SecurityModeDefaults, mode_label: str, ca_model: type[CaModel], device_model: type[DeviceModel], ) -> list[str]: """Return violations for CAs/devices using a no-onboarding protocol blocked by the target mode.""" permitted: list[int] = defaults['permitted_no_onboarding_pki_protocols'] proto_labels: dict[int, str] = {c.value: str(c.label) for c in NoOnboardingPkiProtocol} violations: list[str] = [] for proto in NoOnboardingPkiProtocol: if proto.value in permitted: continue label = proto_labels.get(proto.value, str(proto.value)) for ca_name, bitmask in ca_model.objects.filter( no_onboarding_config__isnull=False ).values_list('unique_name', 'no_onboarding_config__pki_protocols'): if bitmask is not None and (bitmask & proto.value) == proto.value: violations.append(str(_( 'Issuing CA "%(ca)s" uses no-onboarding protocol "%(protocol)s", ' 'which is not permitted in %(mode)s.' ) % {'ca': ca_name, 'protocol': label, 'mode': mode_label})) for dev_name, bitmask in device_model.objects.filter( no_onboarding_config__isnull=False ).values_list('common_name', 'no_onboarding_config__pki_protocols'): if bitmask is not None and (bitmask & proto.value) == proto.value: violations.append(str(_( 'Device "%(device)s" uses no-onboarding protocol "%(protocol)s", ' 'which is not permitted in %(mode)s.' ) % {'device': dev_name, 'protocol': label, 'mode': mode_label})) return violations @staticmethod def _check_onboarding_protocols( defaults: _SecurityModeDefaults, mode_label: str, device_model: type[DeviceModel], ) -> list[str]: """Return violations for devices using an onboarding protocol blocked by the target mode.""" permitted: list[int] = defaults['permitted_onboarding_protocols'] proto_labels: dict[int, str] = {c.value: str(c.label) for c in OnboardingProtocol} rows = device_model.objects.filter( onboarding_config__isnull=False ).exclude( onboarding_config__onboarding_protocol__in=permitted ).values_list('common_name', 'onboarding_config__onboarding_protocol') return [ str(_( 'Device "%(device)s" uses onboarding protocol "%(protocol)s", ' 'which is not permitted in %(mode)s.' ) % {'device': n, 'protocol': proto_labels.get(p, str(p)), 'mode': mode_label}) for n, p in rows ]