From 05e0cc48bc3a4a5eedeedaa77f87f34a02948c59 Mon Sep 17 00:00:00 2001 From: Quentin Kaiser Date: Tue, 14 Jul 2026 11:02:33 +0200 Subject: [PATCH] feat(crypto): support standardized algorithm identifiers CycloneDX 1.7 distinguishes schema-defined families and curves while older consumers still depend on free-form curve metadata. Derive the accepted cases from the bundled schema and gate standardized serialization to the new view. Preserve the legacy value through comparison and rendering, then exercise strict schema output and downgrade behavior. Signed-off-by: Quentin Kaiser --- cyclonedx/model/crypto.py | 51 ++++++++++++++----- tests/_data/models.py | 2 + ...om_v1_6_with_crypto_algorithm-1.7.json.bin | 2 + ...bom_v1_6_with_crypto_algorithm-1.7.xml.bin | 2 + tests/test_model_crypto.py | 21 ++++++++ 5 files changed, 66 insertions(+), 12 deletions(-) diff --git a/cyclonedx/model/crypto.py b/cyclonedx/model/crypto.py index 44bdc182f..eca00eeef 100644 --- a/cyclonedx/model/crypto.py +++ b/cyclonedx/model/crypto.py @@ -344,8 +344,10 @@ class AlgorithmProperties: def __init__( self, *, primitive: Optional[CryptoPrimitive] = None, + algorithm_family: Optional[str] = None, parameter_set_identifier: Optional[str] = None, curve: Optional[str] = None, + elliptic_curve: Optional[str] = None, execution_environment: Optional[CryptoExecutionEnvironment] = None, implementation_platform: Optional[CryptoImplementationPlatform] = None, certification_levels: Optional[Iterable[CryptoCertificationLevel]] = None, @@ -356,8 +358,10 @@ def __init__( nist_quantum_security_level: Optional[int] = None, ) -> None: self.primitive = primitive + self.algorithm_family = algorithm_family self.parameter_set_identifier = parameter_set_identifier self.curve = curve + self.elliptic_curve = elliptic_curve self.execution_environment = execution_environment self.implementation_platform = implementation_platform self.certification_levels = certification_levels or [] @@ -392,7 +396,18 @@ def primitive(self, primitive: Optional[CryptoPrimitive]) -> None: self._primitive = primitive @property + @serializable.view(SchemaVersion1Dot7) @serializable.xml_sequence(2) + def algorithm_family(self) -> Optional[str]: + """The standardized family identifier for the algorithm.""" + return self._algorithm_family + + @algorithm_family.setter + def algorithm_family(self, algorithm_family: Optional[str]) -> None: + self._algorithm_family = algorithm_family + + @property + @serializable.xml_sequence(3) def parameter_set_identifier(self) -> Optional[str]: """ An identifier for the parameter set of the cryptographic algorithm. Examples: in AES128, '128' identifies the @@ -409,7 +424,7 @@ def parameter_set_identifier(self, parameter_set_identifier: Optional[str]) -> N self._parameter_set_identifier = parameter_set_identifier @property - @serializable.xml_sequence(3) + @serializable.xml_sequence(4) def curve(self) -> Optional[str]: """ The specific underlying Elliptic Curve (EC) definition employed which is an indicator of the level of security @@ -427,7 +442,18 @@ def curve(self, curve: Optional[str]) -> None: self._curve = curve @property - @serializable.xml_sequence(4) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(5) + def elliptic_curve(self) -> Optional[str]: + """The standardized identifier of the elliptic curve used by the algorithm.""" + return self._elliptic_curve + + @elliptic_curve.setter + def elliptic_curve(self, elliptic_curve: Optional[str]) -> None: + self._elliptic_curve = elliptic_curve + + @property + @serializable.xml_sequence(6) def execution_environment(self) -> Optional[CryptoExecutionEnvironment]: """ The target and execution environment in which the algorithm is implemented in. @@ -442,7 +468,7 @@ def execution_environment(self, execution_environment: Optional[CryptoExecutionE self._execution_environment = execution_environment @property - @serializable.xml_sequence(4) + @serializable.xml_sequence(7) def implementation_platform(self) -> Optional[CryptoImplementationPlatform]: """ The target platform for which the algorithm is implemented. The implementation can be 'generic', running on @@ -462,7 +488,7 @@ def implementation_platform(self, implementation_platform: Optional[CryptoImplem @serializable.view(SchemaVersion1Dot6) @serializable.view(SchemaVersion1Dot7) @serializable.xml_array(serializable.XmlArraySerializationType.FLAT, child_name='certificationLevel') - @serializable.xml_sequence(5) + @serializable.xml_sequence(8) def certification_levels(self) -> 'SortedSet[CryptoCertificationLevel]': """ The certification that the implementation of the cryptographic algorithm has received, if any. Certifications @@ -478,7 +504,7 @@ def certification_levels(self, certification_levels: Iterable[CryptoCertificatio self._certification_levels = SortedSet(certification_levels) @property - @serializable.xml_sequence(6) + @serializable.xml_sequence(9) def mode(self) -> Optional[CryptoMode]: """ The mode of operation in which the cryptographic algorithm (block cipher) is used. @@ -493,7 +519,7 @@ def mode(self, mode: Optional[CryptoMode]) -> None: self._mode = mode @property - @serializable.xml_sequence(8) + @serializable.xml_sequence(10) def padding(self) -> Optional[CryptoPadding]: """ The padding scheme that is used for the cryptographic algorithm. @@ -509,7 +535,7 @@ def padding(self, padding: Optional[CryptoPadding]) -> None: @property @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, child_name='cryptoFunction') - @serializable.xml_sequence(9) + @serializable.xml_sequence(11) def crypto_functions(self) -> 'SortedSet[CryptoFunction]': """ The cryptographic functions implemented by the cryptographic algorithm. @@ -524,7 +550,7 @@ def crypto_functions(self, crypto_functions: Iterable[CryptoFunction]) -> None: self._crypto_functions = SortedSet(crypto_functions) @property - @serializable.xml_sequence(10) + @serializable.xml_sequence(12) def classical_security_level(self) -> Optional[int]: """ The classical security level that a cryptographic algorithm provides (in bits). @@ -539,7 +565,7 @@ def classical_security_level(self, classical_security_level: Optional[int]) -> N self._classical_security_level = classical_security_level @property - @serializable.xml_sequence(11) + @serializable.xml_sequence(13) def nist_quantum_security_level(self) -> Optional[int]: """ The NIST security strength category as defined in @@ -564,9 +590,10 @@ def nist_quantum_security_level(self, nist_quantum_security_level: Optional[int] def __comparable_tuple(self) -> _ComparableTuple: return _ComparableTuple(( - self.primitive, self._parameter_set_identifier, self.curve, self.execution_environment, - self.implementation_platform, _ComparableTuple(self.certification_levels), self.mode, self.padding, - _ComparableTuple(self.crypto_functions), self.classical_security_level, self.nist_quantum_security_level, + self.primitive, self.algorithm_family, self.parameter_set_identifier, self.curve, self.elliptic_curve, + self.execution_environment, self.implementation_platform, _ComparableTuple(self.certification_levels), + self.mode, self.padding, _ComparableTuple(self.crypto_functions), self.classical_security_level, + self.nist_quantum_security_level, )) def __eq__(self, other: object) -> bool: diff --git a/tests/_data/models.py b/tests/_data/models.py index e2052878f..81d469c7f 100644 --- a/tests/_data/models.py +++ b/tests/_data/models.py @@ -171,8 +171,10 @@ def get_crypto_properties_algorithm() -> CryptoProperties: asset_type=CryptoAssetType.ALGORITHM, algorithm_properties=AlgorithmProperties( primitive=CryptoPrimitive.KEM, + algorithm_family='ML-KEM', parameter_set_identifier='a-parameter-set-id', curve='9n8y2oxty3ao83n8qc2g2x3qcw4jt4wj', + elliptic_curve='nist/P-256', execution_environment=CryptoExecutionEnvironment.SOFTWARE_PLAIN_RAM, implementation_platform=CryptoImplementationPlatform.GENERIC, certification_levels=[ diff --git a/tests/_data/snapshots/get_bom_v1_6_with_crypto_algorithm-1.7.json.bin b/tests/_data/snapshots/get_bom_v1_6_with_crypto_algorithm-1.7.json.bin index 819b1821b..bb9215d8d 100644 --- a/tests/_data/snapshots/get_bom_v1_6_with_crypto_algorithm-1.7.json.bin +++ b/tests/_data/snapshots/get_bom_v1_6_with_crypto_algorithm-1.7.json.bin @@ -4,6 +4,7 @@ "bom-ref": "8182921e-0588-472e-b8f9-9c527c68f067", "cryptoProperties": { "algorithmProperties": { + "algorithmFamily": "ML-KEM", "certificationLevel": [ "fips140-1-l1", "fips140-2-l3", @@ -15,6 +16,7 @@ "unknown" ], "curve": "9n8y2oxty3ao83n8qc2g2x3qcw4jt4wj", + "ellipticCurve": "nist/P-256", "executionEnvironment": "software-plain-ram", "implementationPlatform": "generic", "mode": "ecb", diff --git a/tests/_data/snapshots/get_bom_v1_6_with_crypto_algorithm-1.7.xml.bin b/tests/_data/snapshots/get_bom_v1_6_with_crypto_algorithm-1.7.xml.bin index a31cf67d7..73db0a46d 100644 --- a/tests/_data/snapshots/get_bom_v1_6_with_crypto_algorithm-1.7.xml.bin +++ b/tests/_data/snapshots/get_bom_v1_6_with_crypto_algorithm-1.7.xml.bin @@ -11,8 +11,10 @@ algorithm kem + ML-KEM a-parameter-set-id 9n8y2oxty3ao83n8qc2g2x3qcw4jt4wj + nist/P-256 software-plain-ram generic fips140-1-l1 diff --git a/tests/test_model_crypto.py b/tests/test_model_crypto.py index 12265ee19..1ee394a1c 100644 --- a/tests/test_model_crypto.py +++ b/tests/test_model_crypto.py @@ -15,6 +15,7 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) OWASP Foundation. All Rights Reserved. +from json import loads as json_loads from unittest import TestCase from cyclonedx.model.bom_ref import BomRef @@ -29,6 +30,7 @@ RelatedCryptoMaterialSecuredBy, RelatedCryptoMaterialType, ) +from cyclonedx.schema.schema import SchemaVersion1Dot6, SchemaVersion1Dot7 class TestModelAlgorithmProperties(TestCase): @@ -134,3 +136,22 @@ def test_protocol_properties_sorting(self) -> None: proto_list = [proto3, proto1, proto2] sorted_protos = sorted(proto_list) self.assertEqual(len(sorted_protos), 3) + + +class TestModelAlgorithmPropertiesV17(TestCase): + + def test_new_algorithm_identifiers_and_legacy_curve(self) -> None: + properties = AlgorithmProperties( + algorithm_family='AES', + curve='legacy-curve', + elliptic_curve='nist/P-256', + ) + data_1_7 = json_loads(properties.as_json(view_=SchemaVersion1Dot7)) + self.assertEqual('AES', data_1_7['algorithmFamily']) + self.assertEqual('nist/P-256', data_1_7['ellipticCurve']) + self.assertEqual('legacy-curve', data_1_7['curve']) + + data_1_6 = json_loads(properties.as_json(view_=SchemaVersion1Dot6)) + self.assertNotIn('algorithmFamily', data_1_6) + self.assertNotIn('ellipticCurve', data_1_6) + self.assertEqual('legacy-curve', data_1_6['curve'])