diff --git a/cyclonedx/model/crypto.py b/cyclonedx/model/crypto.py index 44bdc182..a77bbe2b 100644 --- a/cyclonedx/model/crypto.py +++ b/cyclonedx/model/crypto.py @@ -29,13 +29,16 @@ from datetime import datetime from enum import Enum from typing import Any, Optional +from xml.etree.ElementTree import Element # nosec B405 import py_serializable as serializable from sortedcontainers import SortedSet from .._internal.compare import ComparableTuple as _ComparableTuple from ..exception.model import InvalidNistQuantumSecurityLevelException, InvalidRelatedCryptoMaterialSizeException +from ..exception.serialization import CycloneDxDeserializationException from ..schema.schema import SchemaVersion1Dot6, SchemaVersion1Dot7 +from . import HashType from .bom_ref import BomRef @@ -586,6 +589,356 @@ def __repr__(self) -> str: return f'' +@serializable.serializable_enum +class CertificateLifecycleState(str, Enum): + """A pre-defined state in the certificate lifecycle.""" + + PRE_ACTIVATION = 'pre-activation' + ACTIVE = 'active' + SUSPENDED = 'suspended' + DEACTIVATED = 'deactivated' + REVOKED = 'revoked' + DESTROYED = 'destroyed' + + +@serializable.serializable_class(ignore_unknown_during_deserialization=True) +class _CertificateState: + """Non-public common type and deserialization discriminator for certificate states.""" + + @classmethod + def from_json(cls, data: dict[str, Any]) -> '_CertificateState': + if 'state' in data: + return CertificatePredefinedState.from_json(data) + if 'name' in data: + return CertificateCustomState.from_json(data) + raise CycloneDxDeserializationException(f'unexpected certificate state: {data!r}') + + @classmethod + def from_xml(cls, data: Element, default_namespace: Optional[str] = None) -> '_CertificateState': + child_names = {child.tag.rsplit('}', 1)[-1] for child in data} + if 'state' in child_names: + return CertificatePredefinedState.from_xml(data, default_namespace) + if 'name' in child_names: + return CertificateCustomState.from_xml(data, default_namespace) + raise CycloneDxDeserializationException(f'unexpected certificate state: {data!r}') + + def _comparable_tuple(self) -> _ComparableTuple: + raise NotImplementedError() + + def __eq__(self, other: object) -> bool: + return isinstance(other, _CertificateState) and self._comparable_tuple() == other._comparable_tuple() + + def __lt__(self, other: object) -> bool: + if isinstance(other, _CertificateState): + return self._comparable_tuple() < other._comparable_tuple() + return NotImplemented + + def __hash__(self) -> int: + return hash(self._comparable_tuple()) + + +@serializable.serializable_class(ignore_unknown_during_deserialization=True) +class CertificatePredefinedState(_CertificateState): + """A standardized certificate lifecycle state with an optional reason.""" + + def __init__(self, *, state: CertificateLifecycleState, reason: Optional[str] = None) -> None: + self.state = state + self.reason = reason + + @property + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(10) + def state(self) -> CertificateLifecycleState: + return self._state + + @state.setter + def state(self, state: CertificateLifecycleState) -> None: + self._state = state + + @property + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(20) + def reason(self) -> Optional[str]: + return self._reason + + @reason.setter + def reason(self, reason: Optional[str]) -> None: + self._reason = reason + + def _comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(('predefined', self.state, self.reason)) + + def __repr__(self) -> str: + return f'' + + +@serializable.serializable_class(ignore_unknown_during_deserialization=True) +class CertificateCustomState(_CertificateState): + """An application-defined certificate lifecycle state.""" + + def __init__(self, *, name: str, description: Optional[str] = None, reason: Optional[str] = None) -> None: + self.name = name + self.description = description + self.reason = reason + + @property + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(10) + def name(self) -> str: + return self._name + + @name.setter + def name(self, name: str) -> None: + self._name = name + + @property + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(20) + def description(self) -> Optional[str]: + return self._description + + @description.setter + def description(self, description: Optional[str]) -> None: + self._description = description + + @property + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(30) + def reason(self) -> Optional[str]: + return self._reason + + @reason.setter + def reason(self, reason: Optional[str]) -> None: + self._reason = reason + + def _comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(('custom', self.name, self.description, self.reason)) + + def __repr__(self) -> str: + return f'' + + +def _certificate_state_from_json(cls: type[_CertificateState], data: dict[str, Any]) -> _CertificateState: + if 'state' in data: + return CertificatePredefinedState.from_json(data) + if 'name' in data: + return CertificateCustomState.from_json(data) + raise CycloneDxDeserializationException(f'unexpected certificate state: {data!r}') + + +def _certificate_state_from_xml( + cls: type[_CertificateState], data: Element, default_namespace: Optional[str] = None +) -> _CertificateState: + child_names = {child.tag.rsplit('}', 1)[-1] for child in data} + if 'state' in child_names: + return CertificatePredefinedState.from_xml(data, default_namespace) + if 'name' in child_names: + return CertificateCustomState.from_xml(data, default_namespace) + raise CycloneDxDeserializationException(f'unexpected certificate state: {data!r}') + + +_CertificateState.from_json = classmethod(_certificate_state_from_json) # type:ignore[assignment] +_CertificateState.from_xml = classmethod(_certificate_state_from_xml) # type:ignore[assignment] + + +@serializable.serializable_enum +class CertificateCommonExtensionName(str, Enum): + """Names of common X.509 certificate extensions supported by CycloneDX 1.7.""" + + BASIC_CONSTRAINTS = 'basicConstraints' + KEY_USAGE = 'keyUsage' + EXTENDED_KEY_USAGE = 'extendedKeyUsage' + SUBJECT_ALTERNATIVE_NAME = 'subjectAlternativeName' + AUTHORITY_KEY_IDENTIFIER = 'authorityKeyIdentifier' + SUBJECT_KEY_IDENTIFIER = 'subjectKeyIdentifier' + AUTHORITY_INFORMATION_ACCESS = 'authorityInformationAccess' + CERTIFICATE_POLICIES = 'certificatePolicies' + CRL_DISTRIBUTION_POINTS = 'crlDistributionPoints' + SIGNED_CERTIFICATE_TIMESTAMP = 'signedCertificateTimestamp' + + +@serializable.serializable_class(ignore_unknown_during_deserialization=True) +class _CertificateExtension: + """Non-public common type and deserialization discriminator for certificate extensions.""" + + @classmethod + def from_json(cls, data: dict[str, Any]) -> '_CertificateExtension': + if 'commonExtensionName' in data: + return CertificateCommonExtension.from_json(data) + if 'customExtensionName' in data: + return CertificateCustomExtension.from_json(data) + raise CycloneDxDeserializationException(f'unexpected certificate extension: {data!r}') + + @classmethod + def from_xml(cls, data: Element, default_namespace: Optional[str] = None) -> '_CertificateExtension': + child_names = {child.tag.rsplit('}', 1)[-1] for child in data} + if 'commonExtensionName' in child_names: + return CertificateCommonExtension.from_xml(data, default_namespace) + if 'customExtensionName' in child_names: + return CertificateCustomExtension.from_xml(data, default_namespace) + raise CycloneDxDeserializationException(f'unexpected certificate extension: {data!r}') + + def _comparable_tuple(self) -> _ComparableTuple: + raise NotImplementedError() + + def __eq__(self, other: object) -> bool: + return isinstance(other, _CertificateExtension) and self._comparable_tuple() == other._comparable_tuple() + + def __lt__(self, other: object) -> bool: + if isinstance(other, _CertificateExtension): + return self._comparable_tuple() < other._comparable_tuple() + return NotImplemented + + def __hash__(self) -> int: + return hash(self._comparable_tuple()) + + +@serializable.serializable_class(ignore_unknown_during_deserialization=True) +class CertificateCommonExtension(_CertificateExtension): + """A standardized certificate extension and its value.""" + + def __init__( + self, *, + common_extension_name: CertificateCommonExtensionName, + common_extension_value: str, + ) -> None: + self.common_extension_name = common_extension_name + self.common_extension_value = common_extension_value + + @property + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(10) + def common_extension_name(self) -> CertificateCommonExtensionName: + return self._common_extension_name + + @common_extension_name.setter + def common_extension_name(self, common_extension_name: CertificateCommonExtensionName) -> None: + self._common_extension_name = common_extension_name + + @property + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(20) + def common_extension_value(self) -> str: + return self._common_extension_value + + @common_extension_value.setter + def common_extension_value(self, common_extension_value: str) -> None: + self._common_extension_value = common_extension_value + + def _comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(('common', self.common_extension_name, self.common_extension_value)) + + def __repr__(self) -> str: + return f'' + + +@serializable.serializable_class(ignore_unknown_during_deserialization=True) +class CertificateCustomExtension(_CertificateExtension): + """An application- or vendor-defined certificate extension.""" + + def __init__(self, *, custom_extension_name: str, custom_extension_value: Optional[str] = None) -> None: + self.custom_extension_name = custom_extension_name + self.custom_extension_value = custom_extension_value + + @property + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(10) + def custom_extension_name(self) -> str: + return self._custom_extension_name + + @custom_extension_name.setter + def custom_extension_name(self, custom_extension_name: str) -> None: + self._custom_extension_name = custom_extension_name + + @property + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(20) + def custom_extension_value(self) -> Optional[str]: + return self._custom_extension_value + + @custom_extension_value.setter + def custom_extension_value(self, custom_extension_value: Optional[str]) -> None: + self._custom_extension_value = custom_extension_value + + def _comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(('custom', self.custom_extension_name, self.custom_extension_value)) + + def __repr__(self) -> str: + return f'' + + +def _certificate_extension_from_json( + cls: type[_CertificateExtension], data: dict[str, Any] +) -> _CertificateExtension: + if 'commonExtensionName' in data: + return CertificateCommonExtension.from_json(data) + if 'customExtensionName' in data: + return CertificateCustomExtension.from_json(data) + raise CycloneDxDeserializationException(f'unexpected certificate extension: {data!r}') + + +def _certificate_extension_from_xml( + cls: type[_CertificateExtension], data: Element, default_namespace: Optional[str] = None +) -> _CertificateExtension: + child_names = {child.tag.rsplit('}', 1)[-1] for child in data} + if 'commonExtensionName' in child_names: + return CertificateCommonExtension.from_xml(data, default_namespace) + if 'customExtensionName' in child_names: + return CertificateCustomExtension.from_xml(data, default_namespace) + raise CycloneDxDeserializationException(f'unexpected certificate extension: {data!r}') + + +_CertificateExtension.from_json = classmethod(_certificate_extension_from_json) # type:ignore[assignment] +_CertificateExtension.from_xml = classmethod(_certificate_extension_from_xml) # type:ignore[assignment] + + +@serializable.serializable_class(ignore_unknown_during_deserialization=True) +class RelatedCryptographicAsset: + """A typed reference to another cryptographic asset.""" + + def __init__(self, *, type: Optional[str] = None, ref: Optional[BomRef] = None) -> None: + self.type = type + self.ref = ref + + @property + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(10) + def type(self) -> Optional[str]: + return self._type + + @type.setter + def type(self, type: Optional[str]) -> None: + self._type = type + + @property + @serializable.type_mapping(BomRef) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(20) + def ref(self) -> Optional[BomRef]: + return self._ref + + @ref.setter + def ref(self, ref: Optional[BomRef]) -> None: + self._ref = ref + + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple((self.type, self.ref)) + + def __eq__(self, other: object) -> bool: + return isinstance(other, RelatedCryptographicAsset) and self.__comparable_tuple() == other.__comparable_tuple() + + def __lt__(self, other: object) -> bool: + if isinstance(other, RelatedCryptographicAsset): + return self.__comparable_tuple() < other.__comparable_tuple() + return NotImplemented + + def __hash__(self) -> int: + return hash(self.__comparable_tuple()) + + def __repr__(self) -> str: + return f'' + + @serializable.serializable_class(ignore_unknown_during_deserialization=True) class CertificateProperties: """ @@ -602,6 +955,7 @@ class CertificateProperties: def __init__( self, *, + serial_number: Optional[str] = None, subject_name: Optional[str] = None, issuer_name: Optional[str] = None, not_valid_before: Optional[datetime] = None, @@ -610,7 +964,18 @@ def __init__( subject_public_key_ref: Optional[BomRef] = None, certificate_format: Optional[str] = None, certificate_extension: Optional[str] = None, + certificate_file_extension: Optional[str] = None, + fingerprint: Optional[HashType] = None, + certificate_states: Optional[Iterable[_CertificateState]] = None, + creation_date: Optional[datetime] = None, + activation_date: Optional[datetime] = None, + deactivation_date: Optional[datetime] = None, + revocation_date: Optional[datetime] = None, + destruction_date: Optional[datetime] = None, + certificate_extensions: Optional[Iterable[_CertificateExtension]] = None, + related_cryptographic_assets: Optional[Iterable[RelatedCryptographicAsset]] = None, ) -> None: + self.serial_number = serial_number self.subject_name = subject_name self.issuer_name = issuer_name self.not_valid_before = not_valid_before @@ -619,9 +984,30 @@ def __init__( self.subject_public_key_ref = subject_public_key_ref self.certificate_format = certificate_format self.certificate_extension = certificate_extension + self.certificate_file_extension = certificate_file_extension + self.fingerprint = fingerprint + self.certificate_states = certificate_states or [] + self.creation_date = creation_date + self.activation_date = activation_date + self.deactivation_date = deactivation_date + self.revocation_date = revocation_date + self.destruction_date = destruction_date + self.certificate_extensions = certificate_extensions or [] + self.related_cryptographic_assets = related_cryptographic_assets or [] @property + @serializable.view(SchemaVersion1Dot7) @serializable.xml_sequence(10) + def serial_number(self) -> Optional[str]: + """The unique serial number assigned to the certificate by its issuer.""" + return self._serial_number + + @serial_number.setter + def serial_number(self, serial_number: Optional[str]) -> None: + self._serial_number = serial_number + + @property + @serializable.xml_sequence(20) def subject_name(self) -> Optional[str]: """ The subject name for the certificate. @@ -636,7 +1022,7 @@ def subject_name(self, subject_name: Optional[str]) -> None: self._subject_name = subject_name @property - @serializable.xml_sequence(20) + @serializable.xml_sequence(30) def issuer_name(self) -> Optional[str]: """ The issuer name for the certificate. @@ -652,7 +1038,7 @@ def issuer_name(self, issuer_name: Optional[str]) -> None: @property @serializable.type_mapping(serializable.helpers.XsdDateTime) - @serializable.xml_sequence(30) + @serializable.xml_sequence(40) def not_valid_before(self) -> Optional[datetime]: """ The date and time according to ISO-8601 standard from which the certificate is valid. @@ -668,7 +1054,7 @@ def not_valid_before(self, not_valid_before: Optional[datetime]) -> None: @property @serializable.type_mapping(serializable.helpers.XsdDateTime) - @serializable.xml_sequence(40) + @serializable.xml_sequence(50) def not_valid_after(self) -> Optional[datetime]: """ The date and time according to ISO-8601 standard from which the certificate is not valid anymore. @@ -684,7 +1070,7 @@ def not_valid_after(self, not_valid_after: Optional[datetime]) -> None: @property @serializable.type_mapping(BomRef) - @serializable.xml_sequence(50) + @serializable.xml_sequence(60) def signature_algorithm_ref(self) -> Optional[BomRef]: """ The bom-ref to signature algorithm used by the certificate. @@ -700,7 +1086,7 @@ def signature_algorithm_ref(self, signature_algorithm_ref: Optional[BomRef]) -> @property @serializable.type_mapping(BomRef) - @serializable.xml_sequence(60) + @serializable.xml_sequence(70) def subject_public_key_ref(self) -> Optional[BomRef]: """ The bom-ref to the public key of the subject. @@ -715,7 +1101,7 @@ def subject_public_key_ref(self, subject_public_key_ref: Optional[BomRef]) -> No self._subject_public_key_ref = subject_public_key_ref @property - @serializable.xml_sequence(70) + @serializable.xml_sequence(80) def certificate_format(self) -> Optional[str]: """ The format of the certificate. Examples include X.509, PEM, DER, and CVC. @@ -730,7 +1116,7 @@ def certificate_format(self, certificate_format: Optional[str]) -> None: self._certificate_format = certificate_format @property - @serializable.xml_sequence(80) + @serializable.xml_sequence(90) def certificate_extension(self) -> Optional[str]: """ The file extension of the certificate. Examples include crt, pem, cer, der, and p12. @@ -744,16 +1130,139 @@ def certificate_extension(self) -> Optional[str]: def certificate_extension(self, certificate_extension: Optional[str]) -> None: self._certificate_extension = certificate_extension + @property + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(100) + def certificate_file_extension(self) -> Optional[str]: + """The preferred file extension for the certificate.""" + return self._certificate_file_extension + + @certificate_file_extension.setter + def certificate_file_extension(self, certificate_file_extension: Optional[str]) -> None: + self._certificate_file_extension = certificate_file_extension + + @property + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(110) + def fingerprint(self) -> Optional[HashType]: + """A cryptographic hash of the certificate.""" + return self._fingerprint + + @fingerprint.setter + def fingerprint(self, fingerprint: Optional[HashType]) -> None: + self._fingerprint = fingerprint + + @property + @serializable.json_name('certificateState') + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_array(serializable.XmlArraySerializationType.FLAT, 'certificateState') + @serializable.xml_sequence(120) + def certificate_states(self) -> 'SortedSet[_CertificateState]': + """The lifecycle states associated with the certificate.""" + return self._certificate_states + + @certificate_states.setter + def certificate_states(self, certificate_states: Iterable[_CertificateState]) -> None: + self._certificate_states = SortedSet(certificate_states) + + @property + @serializable.type_mapping(serializable.helpers.XsdDateTime) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(130) + def creation_date(self) -> Optional[datetime]: + """The date and time when the certificate was created.""" + return self._creation_date + + @creation_date.setter + def creation_date(self, creation_date: Optional[datetime]) -> None: + self._creation_date = creation_date + + @property + @serializable.type_mapping(serializable.helpers.XsdDateTime) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(140) + def activation_date(self) -> Optional[datetime]: + """The date and time when the certificate became active.""" + return self._activation_date + + @activation_date.setter + def activation_date(self, activation_date: Optional[datetime]) -> None: + self._activation_date = activation_date + + @property + @serializable.type_mapping(serializable.helpers.XsdDateTime) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(150) + def deactivation_date(self) -> Optional[datetime]: + """The date and time when the certificate was deactivated.""" + return self._deactivation_date + + @deactivation_date.setter + def deactivation_date(self, deactivation_date: Optional[datetime]) -> None: + self._deactivation_date = deactivation_date + + @property + @serializable.type_mapping(serializable.helpers.XsdDateTime) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(160) + def revocation_date(self) -> Optional[datetime]: + """The date and time when the certificate was revoked.""" + return self._revocation_date + + @revocation_date.setter + def revocation_date(self, revocation_date: Optional[datetime]) -> None: + self._revocation_date = revocation_date + + @property + @serializable.type_mapping(serializable.helpers.XsdDateTime) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(170) + def destruction_date(self) -> Optional[datetime]: + """The date and time when the certificate was destroyed.""" + return self._destruction_date + + @destruction_date.setter + def destruction_date(self, destruction_date: Optional[datetime]) -> None: + self._destruction_date = destruction_date + + @property + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'certificateExtension') + @serializable.xml_sequence(180) + def certificate_extensions(self) -> 'SortedSet[_CertificateExtension]': + """The X.509 extensions associated with the certificate.""" + return self._certificate_extensions + + @certificate_extensions.setter + def certificate_extensions(self, certificate_extensions: Iterable[_CertificateExtension]) -> None: + self._certificate_extensions = SortedSet(certificate_extensions) + + @property + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'relatedCryptographicAsset') + @serializable.xml_sequence(190) + def related_cryptographic_assets(self) -> 'SortedSet[RelatedCryptographicAsset]': + """Cryptographic assets related to this certificate.""" + return self._related_cryptographic_assets + + @related_cryptographic_assets.setter + def related_cryptographic_assets( + self, related_cryptographic_assets: Iterable[RelatedCryptographicAsset] + ) -> None: + self._related_cryptographic_assets = SortedSet(related_cryptographic_assets) + def __comparable_tuple(self) -> _ComparableTuple: return _ComparableTuple(( - self.subject_name, self.issuer_name, self.not_valid_before, self.not_valid_after, - self.certificate_format, self.certificate_extension + self.serial_number, self.subject_name, self.issuer_name, self.not_valid_before, self.not_valid_after, + self.signature_algorithm_ref, self.subject_public_key_ref, self.certificate_format, + self.certificate_extension, self.certificate_file_extension, self.fingerprint, + _ComparableTuple(self.certificate_states), self.creation_date, self.activation_date, + self.deactivation_date, self.revocation_date, self.destruction_date, + _ComparableTuple(self.certificate_extensions), _ComparableTuple(self.related_cryptographic_assets), )) def __eq__(self, other: object) -> bool: - if isinstance(other, CertificateProperties): - return self.__comparable_tuple() == other.__comparable_tuple() - return False + return isinstance(other, CertificateProperties) and self.__comparable_tuple() == other.__comparable_tuple() def __lt__(self, other: object) -> bool: if isinstance(other, CertificateProperties): @@ -927,6 +1436,8 @@ def __init__( size: Optional[int] = None, format: Optional[str] = None, secured_by: Optional[RelatedCryptoMaterialSecuredBy] = None, + fingerprint: Optional[HashType] = None, + related_cryptographic_assets: Optional[Iterable[RelatedCryptographicAsset]] = None, ) -> None: self.type = type self.id = id @@ -940,6 +1451,8 @@ def __init__( self.size = size self.format = format self.secured_by = secured_by + self.fingerprint = fingerprint + self.related_cryptographic_assets = related_cryptographic_assets or [] @property @serializable.xml_sequence(10) @@ -1126,10 +1639,36 @@ def secured_by(self) -> Optional[RelatedCryptoMaterialSecuredBy]: def secured_by(self, secured_by: Optional[RelatedCryptoMaterialSecuredBy]) -> None: self._secured_by = secured_by + @property + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(130) + def fingerprint(self) -> Optional[HashType]: + """A cryptographic hash of the related material.""" + return self._fingerprint + + @fingerprint.setter + def fingerprint(self, fingerprint: Optional[HashType]) -> None: + self._fingerprint = fingerprint + + @property + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'relatedCryptographicAsset') + @serializable.xml_sequence(140) + def related_cryptographic_assets(self) -> 'SortedSet[RelatedCryptographicAsset]': + """Cryptographic assets related to this material.""" + return self._related_cryptographic_assets + + @related_cryptographic_assets.setter + def related_cryptographic_assets( + self, related_cryptographic_assets: Iterable[RelatedCryptographicAsset] + ) -> None: + self._related_cryptographic_assets = SortedSet(related_cryptographic_assets) + def __comparable_tuple(self) -> _ComparableTuple: return _ComparableTuple(( self.type, self.id, self.state, self.algorithm_ref, self.creation_date, self.activation_date, - self.update_date, self.expiration_date, self.value, self.size, self.format, self.secured_by + self.update_date, self.expiration_date, self.value, self.size, self.format, self.secured_by, + self.fingerprint, _ComparableTuple(self.related_cryptographic_assets), )) def __eq__(self, other: object) -> bool: diff --git a/tests/_data/models.py b/tests/_data/models.py index 565f56ec..b4ddb4bf 100644 --- a/tests/_data/models.py +++ b/tests/_data/models.py @@ -36,6 +36,7 @@ Encoding, ExternalReference, ExternalReferenceType, + HashAlgorithm, HashType, Note, NoteText, @@ -70,6 +71,12 @@ from cyclonedx.model.contact import OrganizationalContact, OrganizationalEntity, PostalAddress from cyclonedx.model.crypto import ( AlgorithmProperties, + CertificateCommonExtension, + CertificateCommonExtensionName, + CertificateCustomExtension, + CertificateCustomState, + CertificateLifecycleState, + CertificatePredefinedState, CertificateProperties, CryptoAssetType, CryptoCertificationLevel, @@ -83,6 +90,7 @@ ProtocolProperties, ProtocolPropertiesCipherSuite, ProtocolPropertiesType, + RelatedCryptographicAsset, RelatedCryptoMaterialProperties, RelatedCryptoMaterialSecuredBy, RelatedCryptoMaterialState, @@ -197,16 +205,49 @@ def get_crypto_properties_certificate() -> CryptoProperties: return CryptoProperties( asset_type=CryptoAssetType.CERTIFICATE, certificate_properties=CertificateProperties( + serial_number='3942447fac867ae5cdb3229b658f4d48', subject_name='cyclonedx.org', issuer_name='Cloudflare Inc ECC CA-3', not_valid_before=datetime(year=2023, month=5, day=19, hour=1, minute=0, second=0, microsecond=0, tzinfo=timezone.utc), not_valid_after=datetime(year=2024, month=5, day=19, hour=0, minute=59, second=59, microsecond=999999, tzinfo=timezone.utc), - signature_algorithm_ref=None, - subject_public_key_ref=None, + signature_algorithm_ref=BomRef('signature-algorithm'), + subject_public_key_ref=BomRef('subject-public-key'), certificate_format='pem', - certificate_extension='csr' + certificate_extension='csr', + certificate_file_extension='pem', + fingerprint=HashType(alg=HashAlgorithm.SHA_256, content='a' * 64), + certificate_states=[ + CertificatePredefinedState( + state=CertificateLifecycleState.ACTIVE, + reason='certificate is in use', + ), + CertificateCustomState( + name='pending-rotation', + description='The certificate is waiting to be rotated.', + reason='scheduled maintenance', + ), + ], + creation_date=datetime(2023, 5, 18, 12, 0, tzinfo=timezone.utc), + activation_date=datetime(2023, 5, 19, 1, 0, tzinfo=timezone.utc), + deactivation_date=datetime(2024, 5, 18, 12, 0, tzinfo=timezone.utc), + revocation_date=datetime(2024, 5, 18, 13, 0, tzinfo=timezone.utc), + destruction_date=datetime(2024, 5, 18, 14, 0, tzinfo=timezone.utc), + certificate_extensions=[ + CertificateCommonExtension( + common_extension_name=CertificateCommonExtensionName.KEY_USAGE, + common_extension_value='digitalSignature', + ), + CertificateCustomExtension( + custom_extension_name='1.2.3.4.5', + custom_extension_value='custom-value', + ), + ], + related_cryptographic_assets=[ + RelatedCryptographicAsset(type='algorithm', ref=BomRef('signature-algorithm')), + RelatedCryptographicAsset(type='publicKey', ref=BomRef('subject-public-key')), + ], ), oid='an-oid-here' ) @@ -268,7 +309,7 @@ def get_crypto_properties_related_material() -> CryptoProperties: type=RelatedCryptoMaterialType.DIGEST, id='some-identifier', state=RelatedCryptoMaterialState.ACTIVE, - algorithm_ref=None, + algorithm_ref=BomRef('material-algorithm'), creation_date=datetime(year=2023, month=5, day=19, hour=1, minute=0, second=0, microsecond=0, tzinfo=timezone.utc), activation_date=datetime(year=2023, month=5, day=19, hour=1, minute=0, second=0, microsecond=0, @@ -281,8 +322,12 @@ def get_crypto_properties_related_material() -> CryptoProperties: format='a-format', secured_by=RelatedCryptoMaterialSecuredBy( mechanism='hard-work', - algorithm_ref=None - ) + algorithm_ref=BomRef('securing-algorithm'), + ), + fingerprint=HashType(alg=HashAlgorithm.SHA_256, content='b' * 64), + related_cryptographic_assets=[ + RelatedCryptographicAsset(type='algorithm', ref=BomRef('material-algorithm')), + ], ), oid='an-oid-here' ) diff --git a/tests/_data/snapshots/enum_CertificateCommonExtensionName-1.7.json.bin b/tests/_data/snapshots/enum_CertificateCommonExtensionName-1.7.json.bin new file mode 100644 index 00000000..e8cb70b0 --- /dev/null +++ b/tests/_data/snapshots/enum_CertificateCommonExtensionName-1.7.json.bin @@ -0,0 +1,214 @@ +{ + "components": [ + { + "bom-ref": "dummy-CCEN:AUTHORITY_INFORMATION_ACCESS", + "cryptoProperties": { + "assetType": "certificate", + "certificateProperties": { + "certificateExtensions": [ + { + "commonExtensionName": "authorityInformationAccess", + "commonExtensionValue": "authorityInformationAccess" + } + ] + } + }, + "name": "CertificateCommonExtensionName: AUTHORITY_INFORMATION_ACCESS", + "type": "cryptographic-asset" + }, + { + "bom-ref": "dummy-CCEN:AUTHORITY_KEY_IDENTIFIER", + "cryptoProperties": { + "assetType": "certificate", + "certificateProperties": { + "certificateExtensions": [ + { + "commonExtensionName": "authorityKeyIdentifier", + "commonExtensionValue": "authorityKeyIdentifier" + } + ] + } + }, + "name": "CertificateCommonExtensionName: AUTHORITY_KEY_IDENTIFIER", + "type": "cryptographic-asset" + }, + { + "bom-ref": "dummy-CCEN:BASIC_CONSTRAINTS", + "cryptoProperties": { + "assetType": "certificate", + "certificateProperties": { + "certificateExtensions": [ + { + "commonExtensionName": "basicConstraints", + "commonExtensionValue": "basicConstraints" + } + ] + } + }, + "name": "CertificateCommonExtensionName: BASIC_CONSTRAINTS", + "type": "cryptographic-asset" + }, + { + "bom-ref": "dummy-CCEN:CERTIFICATE_POLICIES", + "cryptoProperties": { + "assetType": "certificate", + "certificateProperties": { + "certificateExtensions": [ + { + "commonExtensionName": "certificatePolicies", + "commonExtensionValue": "certificatePolicies" + } + ] + } + }, + "name": "CertificateCommonExtensionName: CERTIFICATE_POLICIES", + "type": "cryptographic-asset" + }, + { + "bom-ref": "dummy-CCEN:CRL_DISTRIBUTION_POINTS", + "cryptoProperties": { + "assetType": "certificate", + "certificateProperties": { + "certificateExtensions": [ + { + "commonExtensionName": "crlDistributionPoints", + "commonExtensionValue": "crlDistributionPoints" + } + ] + } + }, + "name": "CertificateCommonExtensionName: CRL_DISTRIBUTION_POINTS", + "type": "cryptographic-asset" + }, + { + "bom-ref": "dummy-CCEN:EXTENDED_KEY_USAGE", + "cryptoProperties": { + "assetType": "certificate", + "certificateProperties": { + "certificateExtensions": [ + { + "commonExtensionName": "extendedKeyUsage", + "commonExtensionValue": "extendedKeyUsage" + } + ] + } + }, + "name": "CertificateCommonExtensionName: EXTENDED_KEY_USAGE", + "type": "cryptographic-asset" + }, + { + "bom-ref": "dummy-CCEN:KEY_USAGE", + "cryptoProperties": { + "assetType": "certificate", + "certificateProperties": { + "certificateExtensions": [ + { + "commonExtensionName": "keyUsage", + "commonExtensionValue": "keyUsage" + } + ] + } + }, + "name": "CertificateCommonExtensionName: KEY_USAGE", + "type": "cryptographic-asset" + }, + { + "bom-ref": "dummy-CCEN:SIGNED_CERTIFICATE_TIMESTAMP", + "cryptoProperties": { + "assetType": "certificate", + "certificateProperties": { + "certificateExtensions": [ + { + "commonExtensionName": "signedCertificateTimestamp", + "commonExtensionValue": "signedCertificateTimestamp" + } + ] + } + }, + "name": "CertificateCommonExtensionName: SIGNED_CERTIFICATE_TIMESTAMP", + "type": "cryptographic-asset" + }, + { + "bom-ref": "dummy-CCEN:SUBJECT_ALTERNATIVE_NAME", + "cryptoProperties": { + "assetType": "certificate", + "certificateProperties": { + "certificateExtensions": [ + { + "commonExtensionName": "subjectAlternativeName", + "commonExtensionValue": "subjectAlternativeName" + } + ] + } + }, + "name": "CertificateCommonExtensionName: SUBJECT_ALTERNATIVE_NAME", + "type": "cryptographic-asset" + }, + { + "bom-ref": "dummy-CCEN:SUBJECT_KEY_IDENTIFIER", + "cryptoProperties": { + "assetType": "certificate", + "certificateProperties": { + "certificateExtensions": [ + { + "commonExtensionName": "subjectKeyIdentifier", + "commonExtensionValue": "subjectKeyIdentifier" + } + ] + } + }, + "name": "CertificateCommonExtensionName: SUBJECT_KEY_IDENTIFIER", + "type": "cryptographic-asset" + } + ], + "dependencies": [ + { + "ref": "dummy-CCEN:AUTHORITY_INFORMATION_ACCESS" + }, + { + "ref": "dummy-CCEN:AUTHORITY_KEY_IDENTIFIER" + }, + { + "ref": "dummy-CCEN:BASIC_CONSTRAINTS" + }, + { + "ref": "dummy-CCEN:CERTIFICATE_POLICIES" + }, + { + "ref": "dummy-CCEN:CRL_DISTRIBUTION_POINTS" + }, + { + "ref": "dummy-CCEN:EXTENDED_KEY_USAGE" + }, + { + "ref": "dummy-CCEN:KEY_USAGE" + }, + { + "ref": "dummy-CCEN:SIGNED_CERTIFICATE_TIMESTAMP" + }, + { + "ref": "dummy-CCEN:SUBJECT_ALTERNATIVE_NAME" + }, + { + "ref": "dummy-CCEN:SUBJECT_KEY_IDENTIFIER" + } + ], + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ], + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.7" +} \ No newline at end of file diff --git a/tests/_data/snapshots/enum_CertificateCommonExtensionName-1.7.xml.bin b/tests/_data/snapshots/enum_CertificateCommonExtensionName-1.7.xml.bin new file mode 100644 index 00000000..cb70f66d --- /dev/null +++ b/tests/_data/snapshots/enum_CertificateCommonExtensionName-1.7.xml.bin @@ -0,0 +1,164 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + + CertificateCommonExtensionName: AUTHORITY_INFORMATION_ACCESS + + certificate + + + + authorityInformationAccess + authorityInformationAccess + + + + + + + CertificateCommonExtensionName: AUTHORITY_KEY_IDENTIFIER + + certificate + + + + authorityKeyIdentifier + authorityKeyIdentifier + + + + + + + CertificateCommonExtensionName: BASIC_CONSTRAINTS + + certificate + + + + basicConstraints + basicConstraints + + + + + + + CertificateCommonExtensionName: CERTIFICATE_POLICIES + + certificate + + + + certificatePolicies + certificatePolicies + + + + + + + CertificateCommonExtensionName: CRL_DISTRIBUTION_POINTS + + certificate + + + + crlDistributionPoints + crlDistributionPoints + + + + + + + CertificateCommonExtensionName: EXTENDED_KEY_USAGE + + certificate + + + + extendedKeyUsage + extendedKeyUsage + + + + + + + CertificateCommonExtensionName: KEY_USAGE + + certificate + + + + keyUsage + keyUsage + + + + + + + CertificateCommonExtensionName: SIGNED_CERTIFICATE_TIMESTAMP + + certificate + + + + signedCertificateTimestamp + signedCertificateTimestamp + + + + + + + CertificateCommonExtensionName: SUBJECT_ALTERNATIVE_NAME + + certificate + + + + subjectAlternativeName + subjectAlternativeName + + + + + + + CertificateCommonExtensionName: SUBJECT_KEY_IDENTIFIER + + certificate + + + + subjectKeyIdentifier + subjectKeyIdentifier + + + + + + + + + + + + + + + + + + + + val1 + val2 + + diff --git a/tests/_data/snapshots/enum_CertificateLifecycleState-1.7.json.bin b/tests/_data/snapshots/enum_CertificateLifecycleState-1.7.json.bin new file mode 100644 index 00000000..d2199136 --- /dev/null +++ b/tests/_data/snapshots/enum_CertificateLifecycleState-1.7.json.bin @@ -0,0 +1,132 @@ +{ + "components": [ + { + "bom-ref": "dummy-CLS:ACTIVE", + "cryptoProperties": { + "assetType": "certificate", + "certificateProperties": { + "certificateState": [ + { + "state": "active" + } + ] + } + }, + "name": "CertificateLifecycleState: ACTIVE", + "type": "cryptographic-asset" + }, + { + "bom-ref": "dummy-CLS:DEACTIVATED", + "cryptoProperties": { + "assetType": "certificate", + "certificateProperties": { + "certificateState": [ + { + "state": "deactivated" + } + ] + } + }, + "name": "CertificateLifecycleState: DEACTIVATED", + "type": "cryptographic-asset" + }, + { + "bom-ref": "dummy-CLS:DESTROYED", + "cryptoProperties": { + "assetType": "certificate", + "certificateProperties": { + "certificateState": [ + { + "state": "destroyed" + } + ] + } + }, + "name": "CertificateLifecycleState: DESTROYED", + "type": "cryptographic-asset" + }, + { + "bom-ref": "dummy-CLS:PRE_ACTIVATION", + "cryptoProperties": { + "assetType": "certificate", + "certificateProperties": { + "certificateState": [ + { + "state": "pre-activation" + } + ] + } + }, + "name": "CertificateLifecycleState: PRE_ACTIVATION", + "type": "cryptographic-asset" + }, + { + "bom-ref": "dummy-CLS:REVOKED", + "cryptoProperties": { + "assetType": "certificate", + "certificateProperties": { + "certificateState": [ + { + "state": "revoked" + } + ] + } + }, + "name": "CertificateLifecycleState: REVOKED", + "type": "cryptographic-asset" + }, + { + "bom-ref": "dummy-CLS:SUSPENDED", + "cryptoProperties": { + "assetType": "certificate", + "certificateProperties": { + "certificateState": [ + { + "state": "suspended" + } + ] + } + }, + "name": "CertificateLifecycleState: SUSPENDED", + "type": "cryptographic-asset" + } + ], + "dependencies": [ + { + "ref": "dummy-CLS:ACTIVE" + }, + { + "ref": "dummy-CLS:DEACTIVATED" + }, + { + "ref": "dummy-CLS:DESTROYED" + }, + { + "ref": "dummy-CLS:PRE_ACTIVATION" + }, + { + "ref": "dummy-CLS:REVOKED" + }, + { + "ref": "dummy-CLS:SUSPENDED" + } + ], + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ], + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.7" +} \ No newline at end of file diff --git a/tests/_data/snapshots/enum_CertificateLifecycleState-1.7.xml.bin b/tests/_data/snapshots/enum_CertificateLifecycleState-1.7.xml.bin new file mode 100644 index 00000000..a718d67e --- /dev/null +++ b/tests/_data/snapshots/enum_CertificateLifecycleState-1.7.xml.bin @@ -0,0 +1,86 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + + CertificateLifecycleState: ACTIVE + + certificate + + + active + + + + + + CertificateLifecycleState: DEACTIVATED + + certificate + + + deactivated + + + + + + CertificateLifecycleState: DESTROYED + + certificate + + + destroyed + + + + + + CertificateLifecycleState: PRE_ACTIVATION + + certificate + + + pre-activation + + + + + + CertificateLifecycleState: REVOKED + + certificate + + + revoked + + + + + + CertificateLifecycleState: SUSPENDED + + certificate + + + suspended + + + + + + + + + + + + + + + val1 + val2 + + diff --git a/tests/_data/snapshots/get_bom_v1_6_with_crypto_certificate-1.6.json.bin b/tests/_data/snapshots/get_bom_v1_6_with_crypto_certificate-1.6.json.bin index bb1fdf24..2e2311a4 100644 --- a/tests/_data/snapshots/get_bom_v1_6_with_crypto_certificate-1.6.json.bin +++ b/tests/_data/snapshots/get_bom_v1_6_with_crypto_certificate-1.6.json.bin @@ -10,7 +10,9 @@ "issuerName": "Cloudflare Inc ECC CA-3", "notValidAfter": "2024-05-19T00:59:59.999999+00:00", "notValidBefore": "2023-05-19T01:00:00+00:00", - "subjectName": "cyclonedx.org" + "signatureAlgorithmRef": "signature-algorithm", + "subjectName": "cyclonedx.org", + "subjectPublicKeyRef": "subject-public-key" }, "oid": "an-oid-here" }, diff --git a/tests/_data/snapshots/get_bom_v1_6_with_crypto_certificate-1.6.xml.bin b/tests/_data/snapshots/get_bom_v1_6_with_crypto_certificate-1.6.xml.bin index 77762892..dec42a5c 100644 --- a/tests/_data/snapshots/get_bom_v1_6_with_crypto_certificate-1.6.xml.bin +++ b/tests/_data/snapshots/get_bom_v1_6_with_crypto_certificate-1.6.xml.bin @@ -14,6 +14,8 @@ Cloudflare Inc ECC CA-3 2023-05-19T01:00:00+00:00 2024-05-19T00:59:59.999999+00:00 + signature-algorithm + subject-public-key pem csr diff --git a/tests/_data/snapshots/get_bom_v1_6_with_crypto_certificate-1.7.json.bin b/tests/_data/snapshots/get_bom_v1_6_with_crypto_certificate-1.7.json.bin index 1ded1772..2078bf2b 100644 --- a/tests/_data/snapshots/get_bom_v1_6_with_crypto_certificate-1.7.json.bin +++ b/tests/_data/snapshots/get_bom_v1_6_with_crypto_certificate-1.7.json.bin @@ -5,12 +5,56 @@ "cryptoProperties": { "assetType": "certificate", "certificateProperties": { + "activationDate": "2023-05-19T01:00:00+00:00", "certificateExtension": "csr", + "certificateExtensions": [ + { + "commonExtensionName": "keyUsage", + "commonExtensionValue": "digitalSignature" + }, + { + "customExtensionName": "1.2.3.4.5", + "customExtensionValue": "custom-value" + } + ], + "certificateFileExtension": "pem", "certificateFormat": "pem", + "certificateState": [ + { + "description": "The certificate is waiting to be rotated.", + "name": "pending-rotation", + "reason": "scheduled maintenance" + }, + { + "reason": "certificate is in use", + "state": "active" + } + ], + "creationDate": "2023-05-18T12:00:00+00:00", + "deactivationDate": "2024-05-18T12:00:00+00:00", + "destructionDate": "2024-05-18T14:00:00+00:00", + "fingerprint": { + "alg": "SHA-256", + "content": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, "issuerName": "Cloudflare Inc ECC CA-3", "notValidAfter": "2024-05-19T00:59:59.999999+00:00", "notValidBefore": "2023-05-19T01:00:00+00:00", - "subjectName": "cyclonedx.org" + "relatedCryptographicAssets": [ + { + "ref": "signature-algorithm", + "type": "algorithm" + }, + { + "ref": "subject-public-key", + "type": "publicKey" + } + ], + "revocationDate": "2024-05-18T13:00:00+00:00", + "serialNumber": "3942447fac867ae5cdb3229b658f4d48", + "signatureAlgorithmRef": "signature-algorithm", + "subjectName": "cyclonedx.org", + "subjectPublicKeyRef": "subject-public-key" }, "oid": "an-oid-here" }, diff --git a/tests/_data/snapshots/get_bom_v1_6_with_crypto_certificate-1.7.xml.bin b/tests/_data/snapshots/get_bom_v1_6_with_crypto_certificate-1.7.xml.bin index 81a2ac3c..cc757649 100644 --- a/tests/_data/snapshots/get_bom_v1_6_with_crypto_certificate-1.7.xml.bin +++ b/tests/_data/snapshots/get_bom_v1_6_with_crypto_certificate-1.7.xml.bin @@ -10,12 +10,51 @@ certificate + 3942447fac867ae5cdb3229b658f4d48 cyclonedx.org Cloudflare Inc ECC CA-3 2023-05-19T01:00:00+00:00 2024-05-19T00:59:59.999999+00:00 + signature-algorithm + subject-public-key pem csr + pem + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + pending-rotation + The certificate is waiting to be rotated. + scheduled maintenance + + + active + certificate is in use + + 2023-05-18T12:00:00+00:00 + 2023-05-19T01:00:00+00:00 + 2024-05-18T12:00:00+00:00 + 2024-05-18T13:00:00+00:00 + 2024-05-18T14:00:00+00:00 + + + keyUsage + digitalSignature + + + 1.2.3.4.5 + custom-value + + + + + algorithm + signature-algorithm + + + publicKey + subject-public-key + + an-oid-here diff --git a/tests/_data/snapshots/get_bom_v1_6_with_crypto_related_material-1.6.json.bin b/tests/_data/snapshots/get_bom_v1_6_with_crypto_related_material-1.6.json.bin index 07cee9c2..708d4234 100644 --- a/tests/_data/snapshots/get_bom_v1_6_with_crypto_related_material-1.6.json.bin +++ b/tests/_data/snapshots/get_bom_v1_6_with_crypto_related_material-1.6.json.bin @@ -7,11 +7,13 @@ "oid": "an-oid-here", "relatedCryptoMaterialProperties": { "activationDate": "2023-05-19T01:00:00+00:00", + "algorithmRef": "material-algorithm", "creationDate": "2023-05-19T01:00:00+00:00", "expirationDate": "2024-05-19T00:59:59.999999+00:00", "format": "a-format", "id": "some-identifier", "securedBy": { + "algorithmRef": "securing-algorithm", "mechanism": "hard-work" }, "size": 32, diff --git a/tests/_data/snapshots/get_bom_v1_6_with_crypto_related_material-1.6.xml.bin b/tests/_data/snapshots/get_bom_v1_6_with_crypto_related_material-1.6.xml.bin index dca04e89..0c8ab3dd 100644 --- a/tests/_data/snapshots/get_bom_v1_6_with_crypto_related_material-1.6.xml.bin +++ b/tests/_data/snapshots/get_bom_v1_6_with_crypto_related_material-1.6.xml.bin @@ -13,6 +13,7 @@ digest some-identifier active + material-algorithm 2023-05-19T01:00:00+00:00 2023-05-19T01:00:00+00:00 2024-05-19T00:59:59.999999+00:00 @@ -21,6 +22,7 @@ a-format hard-work + securing-algorithm an-oid-here diff --git a/tests/_data/snapshots/get_bom_v1_6_with_crypto_related_material-1.7.json.bin b/tests/_data/snapshots/get_bom_v1_6_with_crypto_related_material-1.7.json.bin index 9c404d2e..776e36f8 100644 --- a/tests/_data/snapshots/get_bom_v1_6_with_crypto_related_material-1.7.json.bin +++ b/tests/_data/snapshots/get_bom_v1_6_with_crypto_related_material-1.7.json.bin @@ -7,11 +7,23 @@ "oid": "an-oid-here", "relatedCryptoMaterialProperties": { "activationDate": "2023-05-19T01:00:00+00:00", + "algorithmRef": "material-algorithm", "creationDate": "2023-05-19T01:00:00+00:00", "expirationDate": "2024-05-19T00:59:59.999999+00:00", + "fingerprint": { + "alg": "SHA-256", + "content": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, "format": "a-format", "id": "some-identifier", + "relatedCryptographicAssets": [ + { + "ref": "material-algorithm", + "type": "algorithm" + } + ], "securedBy": { + "algorithmRef": "securing-algorithm", "mechanism": "hard-work" }, "size": 32, diff --git a/tests/_data/snapshots/get_bom_v1_6_with_crypto_related_material-1.7.xml.bin b/tests/_data/snapshots/get_bom_v1_6_with_crypto_related_material-1.7.xml.bin index 239e2c95..1e80dc86 100644 --- a/tests/_data/snapshots/get_bom_v1_6_with_crypto_related_material-1.7.xml.bin +++ b/tests/_data/snapshots/get_bom_v1_6_with_crypto_related_material-1.7.xml.bin @@ -13,6 +13,7 @@ digest some-identifier active + material-algorithm 2023-05-19T01:00:00+00:00 2023-05-19T01:00:00+00:00 2024-05-19T00:59:59.999999+00:00 @@ -21,7 +22,15 @@ a-format hard-work + securing-algorithm + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + + + algorithm + material-algorithm + + an-oid-here diff --git a/tests/test_enums.py b/tests/test_enums.py index 88ac8e71..728a82d7 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -23,7 +23,7 @@ from itertools import chain from json import load as json_load from os import path -from typing import Any, Optional +from typing import Any, Optional, Union from unittest import TestCase from warnings import warn from xml.etree.ElementTree import parse as xml_parse # nosec B405 @@ -94,6 +94,11 @@ VulnerabilitySeverity, ) from cyclonedx.model.crypto import ( # isort:skip + CertificateCommonExtension, + CertificateCommonExtensionName, + CertificateLifecycleState, + CertificatePredefinedState, + CertificateProperties, CryptoAssetType, CryptoCertificationLevel, CryptoExecutionEnvironment, @@ -130,7 +135,7 @@ def dp_cases_from_xml_schemas(xpath: str) -> set[str]: return cases -def dp_cases_from_json_schema(sf: str, jsonpointer: Iterable[str]) -> Generator[str, None, None]: +def dp_cases_from_json_schema(sf: str, jsonpointer: Iterable[Union[str, int]]) -> Generator[str, None, None]: with open(sf) as sfh: data = json_load(sfh) try: @@ -142,7 +147,7 @@ def dp_cases_from_json_schema(sf: str, jsonpointer: Iterable[str]) -> Generator[ yield from data['enum'] -def dp_cases_from_json_schemas(*jsonpointer: str) -> set[str]: +def dp_cases_from_json_schemas(*jsonpointer: Union[str, int]) -> set[str]: cases: set[str] = set() for sf in SCHEMA_JSON.values(): if sf is None: @@ -881,6 +886,68 @@ def test_cases_render_valid(self, of: OutputFormat, sv: SchemaVersion, *_: Any, super()._test_cases_render(bom, of, sv) +@ddt +class TestEnumCertificateLifecycleState(_EnumTestCase): + + @idata(dp_cases_from_json_schemas( + 'definitions', 'cryptoProperties', 'properties', 'certificateProperties', 'properties', + 'certificateState', 'items', 'oneOf', 0, 'properties', 'state' + )) + def test_knows_value(self, value: str) -> None: + super()._test_knows_value(CertificateLifecycleState, value) + + @named_data(*(d for d in NAMED_OF_SV if d[2] is SchemaVersion.V1_7)) + def test_cases_render_valid(self, of: OutputFormat, sv: SchemaVersion, *_: Any, **__: Any) -> None: + bom = _make_bom( + components=[ + Component( + name=f'CertificateLifecycleState: {state.name}', bom_ref=f'dummy-CLS:{state.name}', + type=ComponentType.CRYPTOGRAPHIC_ASSET, + crypto_properties=CryptoProperties( + asset_type=CryptoAssetType.CERTIFICATE, + certificate_properties=CertificateProperties( + certificate_states=[CertificatePredefinedState(state=state)] + ), + ), + ) for state in CertificateLifecycleState + ] + ) + super()._test_cases_render(bom, of, sv) + + +@ddt +class TestEnumCertificateCommonExtensionName(_EnumTestCase): + + @idata(dp_cases_from_json_schemas( + 'definitions', 'cryptoProperties', 'properties', 'certificateProperties', 'properties', + 'certificateExtensions', 'items', 'oneOf', 0, 'properties', 'commonExtensionName' + )) + def test_knows_value(self, value: str) -> None: + super()._test_knows_value(CertificateCommonExtensionName, value) + + @named_data(*(d for d in NAMED_OF_SV if d[2] is SchemaVersion.V1_7)) + def test_cases_render_valid(self, of: OutputFormat, sv: SchemaVersion, *_: Any, **__: Any) -> None: + bom = _make_bom( + components=[ + Component( + name=f'CertificateCommonExtensionName: {extension.name}', + bom_ref=f'dummy-CCEN:{extension.name}', + type=ComponentType.CRYPTOGRAPHIC_ASSET, + crypto_properties=CryptoProperties( + asset_type=CryptoAssetType.CERTIFICATE, + certificate_properties=CertificateProperties( + certificate_extensions=[CertificateCommonExtension( + common_extension_name=extension, + common_extension_value=extension.value, + )] + ), + ), + ) for extension in CertificateCommonExtensionName + ] + ) + super()._test_cases_render(bom, of, sv) + + @ddt class TestEnumRelatedCryptoMaterialType(_EnumTestCase): diff --git a/tests/test_model_crypto.py b/tests/test_model_crypto.py index 12265ee1..0bd51571 100644 --- a/tests/test_model_crypto.py +++ b/tests/test_model_crypto.py @@ -15,20 +15,32 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) OWASP Foundation. All Rights Reserved. +from datetime import datetime, timezone +from json import loads as json_loads from unittest import TestCase +from xml.etree.ElementTree import fromstring as xml_fromstring +from cyclonedx.model import HashAlgorithm, HashType from cyclonedx.model.bom_ref import BomRef from cyclonedx.model.crypto import ( AlgorithmProperties, + CertificateCommonExtension, + CertificateCommonExtensionName, + CertificateCustomExtension, + CertificateCustomState, + CertificateLifecycleState, + CertificatePredefinedState, CertificateProperties, CryptoPrimitive, Ikev2TransformTypes, ProtocolProperties, ProtocolPropertiesType, + RelatedCryptographicAsset, RelatedCryptoMaterialProperties, RelatedCryptoMaterialSecuredBy, RelatedCryptoMaterialType, ) +from cyclonedx.schema.schema import SchemaVersion1Dot6, SchemaVersion1Dot7 class TestModelAlgorithmProperties(TestCase): @@ -47,6 +59,184 @@ def test_algorithm_properties_sorting(self) -> None: class TestModelCertificateProperties(TestCase): + def test_related_assets_are_gated_deterministic_and_preserve_deprecated_refs(self) -> None: + algorithm = RelatedCryptographicAsset(type='algorithm', ref=BomRef('signature')) + public_key = RelatedCryptographicAsset(type='publicKey', ref=BomRef('public-key')) + properties = CertificateProperties( + signature_algorithm_ref=BomRef('signature'), + subject_public_key_ref=BomRef('public-key'), + related_cryptographic_assets=[public_key, algorithm], + ) + + self.assertEqual([algorithm, public_key], list(properties.related_cryptographic_assets)) + self.assertNotEqual(properties, CertificateProperties()) + self.assertNotEqual(hash(properties), hash(CertificateProperties())) + + json_v1_6 = json_loads(properties.as_json(view_=SchemaVersion1Dot6)) + self.assertNotIn('relatedCryptographicAssets', json_v1_6) + self.assertEqual('signature', json_v1_6['signatureAlgorithmRef']) + self.assertEqual('public-key', json_v1_6['subjectPublicKeyRef']) + + json_v1_7 = json_loads(properties.as_json(view_=SchemaVersion1Dot7)) + self.assertEqual('signature', json_v1_7['signatureAlgorithmRef']) + self.assertEqual('public-key', json_v1_7['subjectPublicKeyRef']) + self.assertEqual(properties, CertificateProperties.from_json(json_v1_7)) + + xml_v1_7 = xml_fromstring(properties.as_xml(view_=SchemaVersion1Dot7)) + self.assertEqual(properties, CertificateProperties.from_xml(xml_v1_7)) + + def test_related_cryptographic_asset_comparison_and_hashing(self) -> None: + first = RelatedCryptographicAsset(type='algorithm', ref=BomRef('a')) + second = RelatedCryptographicAsset(type='publicKey', ref=BomRef('b')) + + self.assertNotEqual(first, second) + self.assertNotEqual(hash(first), hash(second)) + self.assertEqual([first, second], sorted([second, first])) + + def test_certificate_extensions_are_gated_deterministic_and_round_trip(self) -> None: + common = CertificateCommonExtension( + common_extension_name=CertificateCommonExtensionName.KEY_USAGE, + common_extension_value='digitalSignature', + ) + custom = CertificateCustomExtension( + custom_extension_name='1.2.3.4.5', + custom_extension_value='custom-value', + ) + properties = CertificateProperties(certificate_extensions=[custom, common]) + + self.assertEqual([common, custom], list(properties.certificate_extensions)) + self.assertNotEqual(properties, CertificateProperties()) + self.assertNotEqual(hash(properties), hash(CertificateProperties())) + self.assertNotIn('certificateExtensions', json_loads(properties.as_json(view_=SchemaVersion1Dot6))) + + json_v1_7 = json_loads(properties.as_json(view_=SchemaVersion1Dot7)) + from_json = CertificateProperties.from_json(json_v1_7) + self.assertEqual(properties, from_json) + self.assertEqual( + {CertificateCommonExtension, CertificateCustomExtension}, + {type(extension) for extension in from_json.certificate_extensions}, + ) + + xml_v1_7 = xml_fromstring(properties.as_xml(view_=SchemaVersion1Dot7)) + from_xml = CertificateProperties.from_xml(xml_v1_7) + self.assertEqual(properties, from_xml) + self.assertEqual( + {CertificateCommonExtension, CertificateCustomExtension}, + {type(extension) for extension in from_xml.certificate_extensions}, + ) + + def test_lifecycle_dates_are_gated_ordered_and_round_trip(self) -> None: + properties = CertificateProperties( + creation_date=datetime(2023, 5, 18, 12, 0, tzinfo=timezone.utc), + activation_date=datetime(2023, 5, 19, 1, 0, tzinfo=timezone.utc), + deactivation_date=datetime(2024, 5, 18, 12, 0, tzinfo=timezone.utc), + revocation_date=datetime(2024, 5, 18, 13, 0, tzinfo=timezone.utc), + destruction_date=datetime(2024, 5, 18, 14, 0, tzinfo=timezone.utc), + ) + + lifecycle_names = [ + 'creationDate', + 'activationDate', + 'deactivationDate', + 'revocationDate', + 'destructionDate', + ] + json_v1_6 = json_loads(properties.as_json(view_=SchemaVersion1Dot6)) + self.assertTrue(all(name not in json_v1_6 for name in lifecycle_names)) + self.assertNotEqual(properties, CertificateProperties()) + self.assertNotEqual(hash(properties), hash(CertificateProperties())) + + json_v1_7 = json_loads(properties.as_json(view_=SchemaVersion1Dot7)) + self.assertEqual(properties, CertificateProperties.from_json(json_v1_7)) + + xml_v1_7 = xml_fromstring(properties.as_xml(view_=SchemaVersion1Dot7)) + self.assertEqual(lifecycle_names, [child.tag for child in xml_v1_7]) + self.assertEqual(properties, CertificateProperties.from_xml(xml_v1_7)) + + def test_certificate_states_are_gated_deterministic_and_round_trip(self) -> None: + predefined = CertificatePredefinedState( + state=CertificateLifecycleState.ACTIVE, + reason='in use', + ) + custom = CertificateCustomState( + name='pending-rotation', + description='custom state', + reason='scheduled maintenance', + ) + properties = CertificateProperties(certificate_states=[predefined, custom]) + + self.assertEqual([custom, predefined], list(properties.certificate_states)) + self.assertNotEqual(properties, CertificateProperties()) + self.assertNotEqual(hash(properties), hash(CertificateProperties())) + self.assertNotIn('certificateState', json_loads(properties.as_json(view_=SchemaVersion1Dot6))) + + json_v1_7 = json_loads(properties.as_json(view_=SchemaVersion1Dot7)) + from_json = CertificateProperties.from_json(json_v1_7) + self.assertEqual(properties, from_json) + self.assertEqual( + {CertificateCustomState, CertificatePredefinedState}, + {type(state) for state in from_json.certificate_states}, + ) + + xml_v1_7 = xml_fromstring(properties.as_xml(view_=SchemaVersion1Dot7)) + from_xml = CertificateProperties.from_xml(xml_v1_7) + self.assertEqual(properties, from_xml) + self.assertEqual( + {CertificateCustomState, CertificatePredefinedState}, + {type(state) for state in from_xml.certificate_states}, + ) + + def test_fingerprint_version_gating_comparison_and_round_trip(self) -> None: + fingerprint = HashType(alg=HashAlgorithm.SHA_256, content='a' * 64) + properties = CertificateProperties(fingerprint=fingerprint) + + self.assertNotEqual(properties, CertificateProperties()) + self.assertNotEqual(hash(properties), hash(CertificateProperties())) + self.assertNotIn('fingerprint', json_loads(properties.as_json(view_=SchemaVersion1Dot6))) + + json_v1_7 = json_loads(properties.as_json(view_=SchemaVersion1Dot7)) + self.assertEqual(properties, CertificateProperties.from_json(json_v1_7)) + xml_v1_7 = xml_fromstring(properties.as_xml(view_=SchemaVersion1Dot7)) + self.assertEqual(properties, CertificateProperties.from_xml(xml_v1_7)) + + def test_certificate_file_extension_preserves_deprecated_extension(self) -> None: + properties = CertificateProperties( + certificate_extension='crt', + certificate_file_extension='pem', + ) + + json_v1_6 = json_loads(properties.as_json(view_=SchemaVersion1Dot6)) + json_v1_7 = json_loads(properties.as_json(view_=SchemaVersion1Dot7)) + self.assertEqual('crt', json_v1_6['certificateExtension']) + self.assertNotIn('certificateFileExtension', json_v1_6) + self.assertEqual('crt', json_v1_7['certificateExtension']) + self.assertEqual('pem', json_v1_7['certificateFileExtension']) + self.assertEqual(properties, CertificateProperties.from_json(json_v1_7)) + + xml_v1_7 = xml_fromstring(properties.as_xml(view_=SchemaVersion1Dot7)) + self.assertEqual(properties, CertificateProperties.from_xml(xml_v1_7)) + + def test_serial_number_construction_and_comparison(self) -> None: + first = CertificateProperties(serial_number='1') + second = CertificateProperties(serial_number='2') + + self.assertEqual('1', first.serial_number) + self.assertNotEqual(first, second) + self.assertNotEqual(hash(first), hash(second)) + self.assertEqual([first, second], sorted([second, first])) + + def test_serial_number_version_gating_and_round_trip(self) -> None: + properties = CertificateProperties(serial_number='3942447fac867ae5cdb3229b658f4d48') + + json_v1_6 = json_loads(properties.as_json(view_=SchemaVersion1Dot6)) + json_v1_7 = json_loads(properties.as_json(view_=SchemaVersion1Dot7)) + self.assertNotIn('serialNumber', json_v1_6) + self.assertEqual(properties.serial_number, json_v1_7['serialNumber']) + self.assertEqual(properties, CertificateProperties.from_json(json_v1_7)) + + xml_v1_7 = xml_fromstring(properties.as_xml(view_=SchemaVersion1Dot7)) + self.assertEqual(properties, CertificateProperties.from_xml(xml_v1_7)) + def test_certificate_properties_sorting(self) -> None: """Test that CertificateProperties instances can be sorted without triggering TypeError""" cert1 = CertificateProperties(subject_name='CN=Test1', certificate_format='X.509') @@ -75,6 +265,47 @@ def test_related_crypto_material_secured_by_sorting(self) -> None: class TestModelRelatedCryptoMaterialProperties(TestCase): + def test_related_assets_are_gated_deterministic_and_preserve_deprecated_refs(self) -> None: + related_asset = RelatedCryptographicAsset(type='algorithm', ref=BomRef('material-algorithm')) + properties = RelatedCryptoMaterialProperties( + algorithm_ref=BomRef('material-algorithm'), + secured_by=RelatedCryptoMaterialSecuredBy( + mechanism='HSM', + algorithm_ref=BomRef('securing-algorithm'), + ), + related_cryptographic_assets=[related_asset], + ) + + self.assertEqual([related_asset], list(properties.related_cryptographic_assets)) + self.assertNotEqual(properties, RelatedCryptoMaterialProperties()) + self.assertNotEqual(hash(properties), hash(RelatedCryptoMaterialProperties())) + + json_v1_6 = json_loads(properties.as_json(view_=SchemaVersion1Dot6)) + self.assertNotIn('relatedCryptographicAssets', json_v1_6) + self.assertEqual('material-algorithm', json_v1_6['algorithmRef']) + self.assertEqual('securing-algorithm', json_v1_6['securedBy']['algorithmRef']) + + json_v1_7 = json_loads(properties.as_json(view_=SchemaVersion1Dot7)) + self.assertEqual('material-algorithm', json_v1_7['algorithmRef']) + self.assertEqual('securing-algorithm', json_v1_7['securedBy']['algorithmRef']) + self.assertEqual(properties, RelatedCryptoMaterialProperties.from_json(json_v1_7)) + + xml_v1_7 = xml_fromstring(properties.as_xml(view_=SchemaVersion1Dot7)) + self.assertEqual(properties, RelatedCryptoMaterialProperties.from_xml(xml_v1_7)) + + def test_fingerprint_version_gating_comparison_and_round_trip(self) -> None: + fingerprint = HashType(alg=HashAlgorithm.SHA_256, content='b' * 64) + properties = RelatedCryptoMaterialProperties(fingerprint=fingerprint) + + self.assertNotEqual(properties, RelatedCryptoMaterialProperties()) + self.assertNotEqual(hash(properties), hash(RelatedCryptoMaterialProperties())) + self.assertNotIn('fingerprint', json_loads(properties.as_json(view_=SchemaVersion1Dot6))) + + json_v1_7 = json_loads(properties.as_json(view_=SchemaVersion1Dot7)) + self.assertEqual(properties, RelatedCryptoMaterialProperties.from_json(json_v1_7)) + xml_v1_7 = xml_fromstring(properties.as_xml(view_=SchemaVersion1Dot7)) + self.assertEqual(properties, RelatedCryptoMaterialProperties.from_xml(xml_v1_7)) + def test_related_crypto_material_properties_sorting(self) -> None: """Test that RelatedCryptoMaterialProperties instances can be sorted without triggering TypeError""" material1 = RelatedCryptoMaterialProperties(