diff --git a/cyclonedx/model/bom.py b/cyclonedx/model/bom.py index 7cb0081e..e0cbc701 100644 --- a/cyclonedx/model/bom.py +++ b/cyclonedx/model/bom.py @@ -51,6 +51,7 @@ from .license import License, LicenseExpression, LicenseRepository, _LicenseRepositorySerializationHelper from .lifecycle import Lifecycle, LifecycleRepository, _LifecycleRepositoryHelper from .service import Service +from .signature import JsfSignature, _JsfSignatureSerializationHelper from .tool import Tool, ToolRepository, _ToolRepositoryHelper from .vulnerability import Vulnerability @@ -444,6 +445,7 @@ def __init__( vulnerabilities: Optional[Iterable[Vulnerability]] = None, properties: Optional[Iterable[Property]] = None, definitions: Optional[Definitions] = None, + signature: Optional[JsfSignature] = None, ) -> None: """ Create a new Bom that you can manually/programmatically add data to later. @@ -461,6 +463,7 @@ def __init__( self.dependencies = dependencies or [] self.properties = properties or [] self.definitions = definitions or Definitions() + self.signature = signature @property @serializable.type_mapping(UrnUuidHelper) @@ -694,6 +697,31 @@ def definitions(self) -> Optional[Definitions]: def definitions(self, definitions: Definitions) -> None: self._definitions = definitions + @property + @serializable.view(SchemaVersion1Dot4) + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.type_mapping(_JsfSignatureSerializationHelper) + def signature(self) -> Optional[JsfSignature]: + """ + Enveloped signature in JSON Signature Format (JSF). + + .. note:: + JSON-only. There is no XSD/XML equivalent in any CycloneDX schema version. + + .. note:: + Introduced in CycloneDX v1.4 + + Returns: + `JsfSignature` if set else `None` + """ + return self._signature + + @signature.setter + def signature(self, signature: Optional[JsfSignature]) -> None: + self._signature = signature + def get_component_by_purl(self, purl: Optional['PackageURL']) -> Optional[Component]: """ Get a Component already in the Bom by its PURL @@ -871,7 +899,7 @@ def __comparable_tuple(self) -> _ComparableTuple: self.components), _ComparableTuple(self.services), _ComparableTuple(self.external_references), _ComparableTuple( self.dependencies), _ComparableTuple(self.properties), - _ComparableTuple(self.vulnerabilities), + _ComparableTuple(self.vulnerabilities), self.signature, )) def __eq__(self, other: object) -> bool: diff --git a/cyclonedx/model/component.py b/cyclonedx/model/component.py index 09031eef..8485c0fc 100644 --- a/cyclonedx/model/component.py +++ b/cyclonedx/model/component.py @@ -70,6 +70,7 @@ from .issue import IssueType from .license import License, LicenseRepository, _LicenseRepositorySerializationHelper from .release_note import ReleaseNotes +from .signature import JsfSignature, _JsfSignatureSerializationHelper @serializable.serializable_class(ignore_unknown_during_deserialization=True) @@ -1012,6 +1013,7 @@ def __init__( swhids: Optional[Iterable[Swhid]] = None, crypto_properties: Optional[CryptoProperties] = None, tags: Optional[Iterable[str]] = None, + signature: Optional[JsfSignature] = None, # Deprecated in v1.6 author: Optional[str] = None, ) -> None: @@ -1042,6 +1044,7 @@ def __init__( self.release_notes = release_notes self.crypto_properties = crypto_properties self.tags = tags or [] + self.signature = signature # spec-deprecated properties below self.author = author self.modified = modified @@ -1659,6 +1662,31 @@ def tags(self) -> 'SortedSet[str]': def tags(self, tags: Iterable[str]) -> None: self._tags = SortedSet(tags) + @property + @serializable.view(SchemaVersion1Dot4) + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.type_mapping(_JsfSignatureSerializationHelper) + def signature(self) -> Optional[JsfSignature]: + """ + Enveloped signature in JSON Signature Format (JSF). + + .. note:: + JSON-only. There is no XSD/XML equivalent in any CycloneDX schema version. + + .. note:: + Introduced in CycloneDX v1.4 + + Returns: + `JsfSignature` if set else `None` + """ + return self._signature + + @signature.setter + def signature(self, signature: Optional[JsfSignature]) -> None: + self._signature = signature + def get_all_nested_components(self, include_self: bool = False) -> set['Component']: components = set() if include_self: @@ -1689,7 +1717,7 @@ def __comparable_tuple(self) -> _ComparableTuple: _ComparableTuple(self.external_references), _ComparableTuple(self.properties), _ComparableTuple(self.components), self.evidence, self.release_notes, self.modified, _ComparableTuple(self.authors), _ComparableTuple(self.omnibor_ids), self.manufacturer, - self.crypto_properties, _ComparableTuple(self.tags), + self.crypto_properties, _ComparableTuple(self.tags), self.signature, )) def __eq__(self, other: object) -> bool: diff --git a/cyclonedx/model/service.py b/cyclonedx/model/service.py index 10597370..3cafeebc 100644 --- a/cyclonedx/model/service.py +++ b/cyclonedx/model/service.py @@ -45,6 +45,7 @@ from .dependency import Dependable from .license import License, LicenseRepository, _LicenseRepositorySerializationHelper from .release_note import ReleaseNotes +from .signature import JsfSignature, _JsfSignatureSerializationHelper @serializable.serializable_class(ignore_unknown_during_deserialization=True) @@ -73,6 +74,7 @@ def __init__( properties: Optional[Iterable[Property]] = None, services: Optional[Iterable['Service']] = None, release_notes: Optional[ReleaseNotes] = None, + signature: Optional[JsfSignature] = None, ) -> None: self._bom_ref = _bom_ref_from_str(bom_ref) self.provider = provider @@ -89,6 +91,7 @@ def __init__( self.services = services or [] self.release_notes = release_notes self.properties = properties or [] + self.signature = signature @property @serializable.json_name('bom-ref') @@ -361,6 +364,31 @@ def release_notes(self) -> Optional[ReleaseNotes]: def release_notes(self, release_notes: Optional[ReleaseNotes]) -> None: self._release_notes = release_notes + @property + @serializable.view(SchemaVersion1Dot4) + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.type_mapping(_JsfSignatureSerializationHelper) + def signature(self) -> Optional[JsfSignature]: + """ + Enveloped signature in JSON Signature Format (JSF). + + .. note:: + JSON-only. There is no XSD/XML equivalent in any CycloneDX schema version. + + .. note:: + Introduced in CycloneDX v1.4 + + Returns: + `JsfSignature` if set else `None` + """ + return self._signature + + @signature.setter + def signature(self, signature: Optional[JsfSignature]) -> None: + self._signature = signature + def __comparable_tuple(self) -> _ComparableTuple: return _ComparableTuple(( self.group, self.name, self.version, @@ -369,7 +397,7 @@ def __comparable_tuple(self) -> _ComparableTuple: self.authenticated, _ComparableTuple(self.data), _ComparableTuple(self.endpoints), _ComparableTuple(self.external_references), _ComparableTuple(self.licenses), _ComparableTuple(self.properties), self.release_notes, _ComparableTuple(self.services), - self.x_trust_boundary + self.x_trust_boundary, self.signature )) def __eq__(self, other: object) -> bool: diff --git a/cyclonedx/model/signature.py b/cyclonedx/model/signature.py new file mode 100644 index 00000000..c4d93bcf --- /dev/null +++ b/cyclonedx/model/signature.py @@ -0,0 +1,455 @@ +# This file is part of CycloneDX Python Library +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. + + +""" +JSF (JSON Signature Format) signature-related classes. + +.. note:: + JSON-only. There is no XSD/XML equivalent for JSF signatures in CycloneDX. + +.. note:: + Introduced in CycloneDX v1.4 + +.. note:: + See the JSF specification: https://cyberphone.github.io/doc/security/jsf.html + See the CycloneDX Schema reference: https://cyclonedx.org/docs/1.4/json/#signature +""" + +from abc import ABC, abstractmethod +from enum import Enum +from typing import Any, Optional, Union +from urllib.parse import urlsplit as _urlsplit +from xml.etree.ElementTree import Element as XmlElement # nosec B405 + +import py_serializable as serializable + +from .._internal.compare import ComparableTuple as _ComparableTuple +from ..exception.model import InvalidValueException + + +@serializable.serializable_enum +class JsfAlgorithm(str, Enum): + """ + Recognized JWA [RFC7518] and RFC8037 asymmetric/symmetric key algorithms for JSF signatures. + + Note: Unlike RFC8037, JSF requires explicit Ed* algorithm names instead of "EdDSA". + + For proprietary algorithms, pass a URI string directly — the ``algorithm`` field on + :class:`JsfSignature`, :class:`JsfSignatureSigners`, and :class:`JsfSignatureChain` + accepts both :class:`JsfAlgorithm` enum values and arbitrary strings. + """ + + RS256 = 'RS256' + RS384 = 'RS384' + RS512 = 'RS512' + PS256 = 'PS256' + PS384 = 'PS384' + PS512 = 'PS512' + ES256 = 'ES256' + ES384 = 'ES384' + ES512 = 'ES512' + ED25519 = 'Ed25519' + ED448 = 'Ed448' + HS256 = 'HS256' + HS384 = 'HS384' + HS512 = 'HS512' + + +@serializable.serializable_enum +class JsfKeyType(str, Enum): + """ + Key type indicator for a JSF public key. + """ + + EC = 'EC' + OKP = 'OKP' + RSA = 'RSA' + + +@serializable.serializable_enum +class JsfEcCurve(str, Enum): + """ + Elliptic curve names for EC public keys in JSF signatures. + + Supported curves per the JSF 0.82 specification. + """ + + P_256 = 'P-256' + P_384 = 'P-384' + P_521 = 'P-521' + + +@serializable.serializable_enum +class JsfOkpCurve(str, Enum): + """ + Curve names for OKP (Octet Key Pair) public keys in JSF signatures. + + Supported curves per the JSF 0.82 specification. + """ + + ED25519 = 'Ed25519' + ED448 = 'Ed448' + + +def _coerce_ec_crv(crv: Union[JsfEcCurve, str]) -> JsfEcCurve: + if isinstance(crv, JsfEcCurve): + return crv + try: + return JsfEcCurve(crv) + except ValueError: + raise InvalidValueException( + f'EC public key crv must be one of {[c.value for c in JsfEcCurve]!r}, got {crv!r}' + ) from None + + +def _coerce_okp_crv(crv: Union[JsfOkpCurve, str]) -> JsfOkpCurve: + if isinstance(crv, JsfOkpCurve): + return crv + try: + return JsfOkpCurve(crv) + except ValueError: + raise InvalidValueException( + f'OKP public key crv must be one of {[c.value for c in JsfOkpCurve]!r}, got {crv!r}' + ) from None + + +def _check_no_rsa_fields(kty: JsfKeyType, n: Optional[str], e: Optional[str]) -> None: + if n is not None or e is not None: + raise InvalidValueException( + f'{kty.value} public key must not include RSA-specific fields (n, e)' + ) + + +def _check_no_y_field(y: Optional[str]) -> None: + if y is not None: + raise InvalidValueException('OKP public key must not include y') + + +def _check_no_ec_okp_fields( + crv: 'Optional[Union[JsfEcCurve, JsfOkpCurve]]', + x: Optional[str], + y: Optional[str], +) -> None: + if crv is not None or x is not None or y is not None: + raise InvalidValueException( + 'RSA public key must not include EC/OKP-specific fields (crv, x, y)' + ) + + +class JsfPublicKey: + """ + Public key object as defined by the JSF standard. + + Supports three key types (determined by ``kty``): + + - **EC**: requires ``crv``, ``x``, ``y`` + - **OKP**: requires ``crv``, ``x`` + - **RSA**: requires ``n``, ``e`` + """ + + def __init__( + self, *, + kty: JsfKeyType, + crv: Optional[Union[JsfEcCurve, JsfOkpCurve]] = None, + x: Optional[str] = None, + y: Optional[str] = None, + n: Optional[str] = None, + e: Optional[str] = None, + ) -> None: + # Validate conditional schema requirements per JSF spec + if kty == JsfKeyType.EC: + if not (crv and x and y): + raise InvalidValueException('EC public key requires crv, x, and y') + if not isinstance(crv, JsfEcCurve): + raise InvalidValueException( + f'EC public key crv must be a JsfEcCurve instance, got {type(crv).__name__!r}' + ) + _check_no_rsa_fields(kty, n, e) + elif kty == JsfKeyType.OKP: + if not (crv and x): + raise InvalidValueException('OKP public key requires crv and x') + if not isinstance(crv, JsfOkpCurve): + raise InvalidValueException( + f'OKP public key crv must be a JsfOkpCurve instance, got {type(crv).__name__!r}' + ) + _check_no_y_field(y) + _check_no_rsa_fields(kty, n, e) + elif kty == JsfKeyType.RSA: + if not (n and e): + raise InvalidValueException('RSA public key requires n and e') + _check_no_ec_okp_fields(crv, x, y) + + self.kty = kty + self.crv: Optional[Union[JsfEcCurve, JsfOkpCurve]] = crv + self.x = x + self.y = y + self.n = n + self.e = e + + def _as_dict(self) -> dict[str, Any]: + d: dict[str, Any] = {'kty': self.kty.value} + if self.crv is not None: + d['crv'] = self.crv.value + if self.x is not None: + d['x'] = self.x + if self.y is not None: + d['y'] = self.y + if self.n is not None: + d['n'] = self.n + if self.e is not None: + d['e'] = self.e + return d + + @classmethod + def _from_dict(cls, d: dict[str, Any]) -> 'JsfPublicKey': + kty = JsfKeyType(d['kty']) + crv_raw = d.get('crv') + crv: Optional[Union[JsfEcCurve, JsfOkpCurve]] = None + if crv_raw is not None: + if kty == JsfKeyType.EC: + crv = _coerce_ec_crv(crv_raw) + elif kty == JsfKeyType.OKP: + crv = _coerce_okp_crv(crv_raw) + return cls(kty=kty, crv=crv, x=d.get('x'), y=d.get('y'), n=d.get('n'), e=d.get('e')) + + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple((self.kty, self.crv, self.x, self.y, self.n, self.e)) + + def __eq__(self, other: object) -> bool: + if isinstance(other, JsfPublicKey): + return self.__comparable_tuple() == other.__comparable_tuple() + return False + + def __lt__(self, other: Any) -> bool: + if isinstance(other, JsfPublicKey): + return self.__comparable_tuple() < other.__comparable_tuple() + return NotImplemented + + def __hash__(self) -> int: + return hash(self.__comparable_tuple()) + + def __repr__(self) -> str: + return f'' + + +class JsfSignature(ABC): + """ + JSF (JSON Signature Format) signature object — abstract base class. + + The JSF specification defines three mutually exclusive signature modes, each represented by + a separate concrete class: + + - **Simple signature** (``signaturecore``): :class:`JsfSimpleSignature`: a single signature + with required ``algorithm`` and ``value``, plus optional ``key_id``, ``public_key``, + ``certificate_path``, and ``excludes`` + - **Multiple signers** (``multisignature``): :class:`JsfSignatureSigners`: contains a + ``signers`` list of :class:`JsfSimpleSignature` + - **Signature chain** (``signaturechain``): :class:`JsfSignatureChain`: contains a + ``chain`` list of :class:`JsfSimpleSignature` + + .. note:: + JSON-only. There is no XSD/XML equivalent in any CycloneDX schema version. + + .. note:: + Introduced in CycloneDX v1.4 + """ + + @abstractmethod + def _as_dict(self) -> dict[str, Any]: + ... # pragma: no cover + + @classmethod + @abstractmethod + def _from_dict(cls, d: dict[str, Any]) -> 'JsfSignature': + ... # pragma: no cover + + def __eq__(self, other: object) -> bool: + if isinstance(other, JsfSignature): + return _JsfSignatureSerializationHelper._sort_key(self) == _JsfSignatureSerializationHelper._sort_key(other) + return False + + def __lt__(self, other: Any) -> bool: + if isinstance(other, JsfSignature): + return _JsfSignatureSerializationHelper._sort_key(self) < _JsfSignatureSerializationHelper._sort_key(other) + return NotImplemented + + def __hash__(self) -> int: + return hash(_JsfSignatureSerializationHelper._sort_key(self)) + + +class JsfSimpleSignature(JsfSignature): + """ + JSF simple signature object: ``signaturecore`` mode. + + Represents a single signature with required ``algorithm`` and ``value``, plus optional + ``key_id``, ``public_key``, ``certificate_path``, and ``excludes``. + """ + + def __init__( + self, *, + algorithm: Union[JsfAlgorithm, str], + value: str, + key_id: Optional[str] = None, + public_key: Optional[JsfPublicKey] = None, + certificate_path: Optional[list[str]] = None, + excludes: Optional[list[str]] = None, + ) -> None: + if not isinstance(algorithm, JsfAlgorithm): + # Proprietary algorithms must be expressed as URIs per JSF spec + if not _urlsplit(str(algorithm)).scheme: + raise InvalidValueException( + f'Proprietary JSF algorithm must be expressed as a URI, got {algorithm!r}' + ) + self.algorithm = algorithm + self.value = value + self.key_id = key_id + self.public_key = public_key + self.certificate_path = list(certificate_path) if certificate_path else [] + self.excludes = list(excludes) if excludes else [] + + def _as_dict(self) -> dict[str, Any]: + d: dict[str, Any] = { + 'algorithm': self.algorithm.value if isinstance(self.algorithm, JsfAlgorithm) else str(self.algorithm), + 'value': self.value, + } + if self.key_id is not None: + d['keyId'] = self.key_id + if self.public_key is not None: + d['publicKey'] = self.public_key._as_dict() + if self.certificate_path: + d['certificatePath'] = list(self.certificate_path) + if self.excludes: + d['excludes'] = list(self.excludes) + return d + + @classmethod + def _from_dict(cls, d: dict[str, Any]) -> 'JsfSimpleSignature': + algorithm: Union[JsfAlgorithm, str] + try: + algorithm = JsfAlgorithm(d['algorithm']) + except ValueError: + algorithm = d['algorithm'] + pk = d.get('publicKey') + return cls( + algorithm=algorithm, + value=d['value'], + key_id=d.get('keyId'), + public_key=JsfPublicKey._from_dict(pk) if pk is not None else None, + certificate_path=d.get('certificatePath'), + excludes=d.get('excludes'), + ) + + def __repr__(self) -> str: + return f'' + + +class JsfSignatureSigners(JsfSignature): + """ + Multiple-signers JSF signature: ``multisignature`` in the JSF schema. + + Contains a list of :class:`JsfSimpleSignature` objects serialized under the ``signers`` key. + """ + + def __init__(self, *, signers: list['JsfSimpleSignature']) -> None: + if not signers: + raise InvalidValueException('JsfSignatureSigners requires at least one signer') + self.signers = list(signers) + + def _as_dict(self) -> dict[str, Any]: + return {'signers': [s._as_dict() for s in self.signers]} + + @classmethod + def _from_dict(cls, d: dict[str, Any]) -> 'JsfSignatureSigners': + return cls(signers=[JsfSimpleSignature._from_dict(s) for s in d['signers']]) + + def __repr__(self) -> str: + return f'' + + +class JsfSignatureChain(JsfSignature): + """ + Signature-chain JSF signature: ``signaturechain`` in the JSF schema. + + Contains a list of :class:`JsfSimpleSignature` objects serialized under the ``chain`` key. + """ + + def __init__(self, *, chain: list['JsfSimpleSignature']) -> None: + if not chain: + raise InvalidValueException('JsfSignatureChain requires at least one element') + self.chain = list(chain) + + def _as_dict(self) -> dict[str, Any]: + return {'chain': [s._as_dict() for s in self.chain]} + + @classmethod + def _from_dict(cls, d: dict[str, Any]) -> 'JsfSignatureChain': + return cls(chain=[JsfSimpleSignature._from_dict(s) for s in d['chain']]) + + def __repr__(self) -> str: + return f'' + + +class _JsfSignatureSerializationHelper(serializable.helpers.BaseHelper): + """ THIS CLASS IS NON-PUBLIC API """ + + @staticmethod + def _sort_key(o: JsfSignature) -> _ComparableTuple: + """Generate a comparable tuple key for sorting and equality. + + Handles all three signature modes by delegating to type-specific logic. + """ + if isinstance(o, JsfSignatureChain): + return _ComparableTuple(('chain', _ComparableTuple(o.chain))) + if isinstance(o, JsfSignatureSigners): + return _ComparableTuple(('signers', _ComparableTuple(o.signers))) + if isinstance(o, JsfSimpleSignature): + algo_str = o.algorithm.value if isinstance(o.algorithm, JsfAlgorithm) else str(o.algorithm) + return _ComparableTuple((algo_str, o.value, o.key_id, o.public_key, + _ComparableTuple(o.certificate_path), + _ComparableTuple(o.excludes))) + raise TypeError(f'Unknown JsfSignature subtype: {type(o)!r}') # pragma: no cover + + @classmethod + def json_normalize(cls, o: JsfSignature, *, + view: Optional[type[serializable.ViewType]], + **__: Any) -> Any: + return o._as_dict() + + @classmethod + def json_denormalize(cls, o: Any, **__: Any) -> JsfSignature: + if not isinstance(o, dict): + raise TypeError(f'Expected dict, got {type(o)!r}') + if 'signers' in o: + return JsfSignatureSigners._from_dict(o) + if 'chain' in o: + return JsfSignatureChain._from_dict(o) + return JsfSimpleSignature._from_dict(o) + + @classmethod + def xml_normalize(cls, o: JsfSignature, *, + element_name: Optional[str], + view: Optional[type[serializable.ViewType]], + xmlns: Optional[str], + **__: Any) -> Optional[XmlElement]: + return None # JSF signatures have no XML representation + + @classmethod + def xml_denormalize(cls, o: XmlElement, *, + default_ns: Optional[str], + **__: Any) -> Optional[JsfSignature]: + return None # JSF signatures have no XML representation diff --git a/tests/_data/models.py b/tests/_data/models.py index 565f56ec..5af00bc5 100644 --- a/tests/_data/models.py +++ b/tests/_data/models.py @@ -107,6 +107,14 @@ from cyclonedx.model.lifecycle import LifecyclePhase, NamedLifecycle, PredefinedLifecycle from cyclonedx.model.release_note import ReleaseNotes from cyclonedx.model.service import Service +from cyclonedx.model.signature import ( + JsfAlgorithm, + JsfKeyType, + JsfPublicKey, + JsfSignatureChain, + JsfSignatureSigners, + JsfSimpleSignature, +) from cyclonedx.model.tool import Tool, ToolRepository from cyclonedx.model.vulnerability import ( BomTarget, @@ -1590,6 +1598,67 @@ def get_bom_for_issue540_duplicate_components() -> Bom: return bom +def get_bom_with_signatures() -> Bom: + # Tests JSF signature support on Bom, Component, and Service (JSON-only, CDX >= 1.4) + simple_sig = JsfSimpleSignature( + algorithm=JsfAlgorithm.ES256, + value='MEQCIAJ9DECTPNuqwdWHlHO3EB1jYVnjW7HZ0T7x3QIDO4OMfAIgTFz5kl3Zl7nBP4r2TovMnbJo3ij6JTANcFAQQVEBZQ==', + key_id='test-key-1', + ) + multi_sig = JsfSignatureSigners( + signers=[ + JsfSimpleSignature( + algorithm=JsfAlgorithm.RS256, + value='AABBCC==', + public_key=JsfPublicKey( + kty=JsfKeyType.RSA, + n='sQ3MDBw==', + e='AQAB', + ), + ), + JsfSimpleSignature( + algorithm=JsfAlgorithm.ES384, + value='DDEEFF==', + certificate_path=['MIICpDCCAYwCCQDU'], + excludes=['signature'], + ), + ] + ) + bom = _make_bom( + components=[ + Component( + name='acme-library', + version='1.2.3', + type=ComponentType.LIBRARY, + bom_ref='acme-library', + signature=simple_sig, + ) + ], + services=[ + Service( + name='acme-service', + bom_ref='acme-service', + signature=JsfSignatureChain( + chain=[ + JsfSimpleSignature( + algorithm=JsfAlgorithm.ED25519, + value='xyzSig==', + ) + ] + ), + ) + ], + signature=multi_sig, + ) + bom.metadata.component = Component( + name='my-app', + version='0.1.0', + type=ComponentType.APPLICATION, + bom_ref='my-app', + ) + return bom + + def get_bom_for_issue941_nested_dependencies_irreversible_migrate() -> Bom: bom = _make_bom() bom.metadata.component = root_component = Component( @@ -1669,9 +1738,21 @@ def get_bom_for_issue941_nested_dependencies_irreversible_migrate() -> Bom: if n.startswith('get_bom_') and not n.endswith('_invalid') ) +all_get_bom_funct_no_xml_roundtrip: frozenset = frozenset({ + # BOMs that contain JSON-only fields (e.g. JSF signatures). + # These are excluded from test_deserialize_xml's test_prepared assertBomDeepEqual comparison. + # Use all_get_bom_funct_no_xml_roundtrip_immut for @named_data. + get_bom_with_signatures, +}) + +all_get_bom_funct_no_xml_roundtrip_immut = tuple( + (f.__name__, f) for f in sorted(all_get_bom_funct_no_xml_roundtrip, key=lambda f: f.__name__) +) + all_get_bom_funct_valid_immut = tuple( (n, f) for n, f in getmembers(sys.modules[__name__], isfunction) if n.startswith('get_bom_') and not n.endswith('_invalid') and not n.endswith('_migrate') + and f not in all_get_bom_funct_no_xml_roundtrip ) all_get_bom_funct_valid_reversible_migrate = tuple( @@ -1710,4 +1791,5 @@ def get_bom_for_issue941_nested_dependencies_irreversible_migrate() -> Bom: get_bom_with_distribution_constraints, get_bom_with_definitions_standards, get_bom_with_definitions_and_detailed_standards, + get_bom_with_signatures, } diff --git a/tests/_data/snapshots/enum_JsfAlgorithm-1.4.json.bin b/tests/_data/snapshots/enum_JsfAlgorithm-1.4.json.bin new file mode 100644 index 00000000..c6945cdd --- /dev/null +++ b/tests/_data/snapshots/enum_JsfAlgorithm-1.4.json.bin @@ -0,0 +1,182 @@ +{ + "components": [ + { + "bom-ref": "dummy-JA:ED25519", + "name": "JsfAlgorithm: ED25519", + "signature": { + "algorithm": "Ed25519", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:ED448", + "name": "JsfAlgorithm: ED448", + "signature": { + "algorithm": "Ed448", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:ES256", + "name": "JsfAlgorithm: ES256", + "signature": { + "algorithm": "ES256", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:ES384", + "name": "JsfAlgorithm: ES384", + "signature": { + "algorithm": "ES384", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:ES512", + "name": "JsfAlgorithm: ES512", + "signature": { + "algorithm": "ES512", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:HS256", + "name": "JsfAlgorithm: HS256", + "signature": { + "algorithm": "HS256", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:HS384", + "name": "JsfAlgorithm: HS384", + "signature": { + "algorithm": "HS384", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:HS512", + "name": "JsfAlgorithm: HS512", + "signature": { + "algorithm": "HS512", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:PS256", + "name": "JsfAlgorithm: PS256", + "signature": { + "algorithm": "PS256", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:PS384", + "name": "JsfAlgorithm: PS384", + "signature": { + "algorithm": "PS384", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:PS512", + "name": "JsfAlgorithm: PS512", + "signature": { + "algorithm": "PS512", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:RS256", + "name": "JsfAlgorithm: RS256", + "signature": { + "algorithm": "RS256", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:RS384", + "name": "JsfAlgorithm: RS384", + "signature": { + "algorithm": "RS384", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:RS512", + "name": "JsfAlgorithm: RS512", + "signature": { + "algorithm": "RS512", + "value": "AABBCC==" + }, + "type": "library" + } + ], + "dependencies": [ + { + "ref": "dummy-JA:ED25519" + }, + { + "ref": "dummy-JA:ED448" + }, + { + "ref": "dummy-JA:ES256" + }, + { + "ref": "dummy-JA:ES384" + }, + { + "ref": "dummy-JA:ES512" + }, + { + "ref": "dummy-JA:HS256" + }, + { + "ref": "dummy-JA:HS384" + }, + { + "ref": "dummy-JA:HS512" + }, + { + "ref": "dummy-JA:PS256" + }, + { + "ref": "dummy-JA:PS384" + }, + { + "ref": "dummy-JA:PS512" + }, + { + "ref": "dummy-JA:RS256" + }, + { + "ref": "dummy-JA:RS384" + }, + { + "ref": "dummy-JA:RS512" + } + ], + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.4" +} \ No newline at end of file diff --git a/tests/_data/snapshots/enum_JsfAlgorithm-1.5.json.bin b/tests/_data/snapshots/enum_JsfAlgorithm-1.5.json.bin new file mode 100644 index 00000000..a5df74fb --- /dev/null +++ b/tests/_data/snapshots/enum_JsfAlgorithm-1.5.json.bin @@ -0,0 +1,192 @@ +{ + "components": [ + { + "bom-ref": "dummy-JA:ED25519", + "name": "JsfAlgorithm: ED25519", + "signature": { + "algorithm": "Ed25519", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:ED448", + "name": "JsfAlgorithm: ED448", + "signature": { + "algorithm": "Ed448", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:ES256", + "name": "JsfAlgorithm: ES256", + "signature": { + "algorithm": "ES256", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:ES384", + "name": "JsfAlgorithm: ES384", + "signature": { + "algorithm": "ES384", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:ES512", + "name": "JsfAlgorithm: ES512", + "signature": { + "algorithm": "ES512", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:HS256", + "name": "JsfAlgorithm: HS256", + "signature": { + "algorithm": "HS256", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:HS384", + "name": "JsfAlgorithm: HS384", + "signature": { + "algorithm": "HS384", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:HS512", + "name": "JsfAlgorithm: HS512", + "signature": { + "algorithm": "HS512", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:PS256", + "name": "JsfAlgorithm: PS256", + "signature": { + "algorithm": "PS256", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:PS384", + "name": "JsfAlgorithm: PS384", + "signature": { + "algorithm": "PS384", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:PS512", + "name": "JsfAlgorithm: PS512", + "signature": { + "algorithm": "PS512", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:RS256", + "name": "JsfAlgorithm: RS256", + "signature": { + "algorithm": "RS256", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:RS384", + "name": "JsfAlgorithm: RS384", + "signature": { + "algorithm": "RS384", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:RS512", + "name": "JsfAlgorithm: RS512", + "signature": { + "algorithm": "RS512", + "value": "AABBCC==" + }, + "type": "library" + } + ], + "dependencies": [ + { + "ref": "dummy-JA:ED25519" + }, + { + "ref": "dummy-JA:ED448" + }, + { + "ref": "dummy-JA:ES256" + }, + { + "ref": "dummy-JA:ES384" + }, + { + "ref": "dummy-JA:ES512" + }, + { + "ref": "dummy-JA:HS256" + }, + { + "ref": "dummy-JA:HS384" + }, + { + "ref": "dummy-JA:HS512" + }, + { + "ref": "dummy-JA:PS256" + }, + { + "ref": "dummy-JA:PS384" + }, + { + "ref": "dummy-JA:PS512" + }, + { + "ref": "dummy-JA:RS256" + }, + { + "ref": "dummy-JA:RS384" + }, + { + "ref": "dummy-JA:RS512" + } + ], + "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.5.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.5" +} \ No newline at end of file diff --git a/tests/_data/snapshots/enum_JsfAlgorithm-1.6.json.bin b/tests/_data/snapshots/enum_JsfAlgorithm-1.6.json.bin new file mode 100644 index 00000000..b586bd9d --- /dev/null +++ b/tests/_data/snapshots/enum_JsfAlgorithm-1.6.json.bin @@ -0,0 +1,192 @@ +{ + "components": [ + { + "bom-ref": "dummy-JA:ED25519", + "name": "JsfAlgorithm: ED25519", + "signature": { + "algorithm": "Ed25519", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:ED448", + "name": "JsfAlgorithm: ED448", + "signature": { + "algorithm": "Ed448", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:ES256", + "name": "JsfAlgorithm: ES256", + "signature": { + "algorithm": "ES256", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:ES384", + "name": "JsfAlgorithm: ES384", + "signature": { + "algorithm": "ES384", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:ES512", + "name": "JsfAlgorithm: ES512", + "signature": { + "algorithm": "ES512", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:HS256", + "name": "JsfAlgorithm: HS256", + "signature": { + "algorithm": "HS256", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:HS384", + "name": "JsfAlgorithm: HS384", + "signature": { + "algorithm": "HS384", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:HS512", + "name": "JsfAlgorithm: HS512", + "signature": { + "algorithm": "HS512", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:PS256", + "name": "JsfAlgorithm: PS256", + "signature": { + "algorithm": "PS256", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:PS384", + "name": "JsfAlgorithm: PS384", + "signature": { + "algorithm": "PS384", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:PS512", + "name": "JsfAlgorithm: PS512", + "signature": { + "algorithm": "PS512", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:RS256", + "name": "JsfAlgorithm: RS256", + "signature": { + "algorithm": "RS256", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:RS384", + "name": "JsfAlgorithm: RS384", + "signature": { + "algorithm": "RS384", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:RS512", + "name": "JsfAlgorithm: RS512", + "signature": { + "algorithm": "RS512", + "value": "AABBCC==" + }, + "type": "library" + } + ], + "dependencies": [ + { + "ref": "dummy-JA:ED25519" + }, + { + "ref": "dummy-JA:ED448" + }, + { + "ref": "dummy-JA:ES256" + }, + { + "ref": "dummy-JA:ES384" + }, + { + "ref": "dummy-JA:ES512" + }, + { + "ref": "dummy-JA:HS256" + }, + { + "ref": "dummy-JA:HS384" + }, + { + "ref": "dummy-JA:HS512" + }, + { + "ref": "dummy-JA:PS256" + }, + { + "ref": "dummy-JA:PS384" + }, + { + "ref": "dummy-JA:PS512" + }, + { + "ref": "dummy-JA:RS256" + }, + { + "ref": "dummy-JA:RS384" + }, + { + "ref": "dummy-JA:RS512" + } + ], + "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.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6" +} \ No newline at end of file diff --git a/tests/_data/snapshots/enum_JsfAlgorithm-1.7.json.bin b/tests/_data/snapshots/enum_JsfAlgorithm-1.7.json.bin new file mode 100644 index 00000000..27497bb0 --- /dev/null +++ b/tests/_data/snapshots/enum_JsfAlgorithm-1.7.json.bin @@ -0,0 +1,192 @@ +{ + "components": [ + { + "bom-ref": "dummy-JA:ED25519", + "name": "JsfAlgorithm: ED25519", + "signature": { + "algorithm": "Ed25519", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:ED448", + "name": "JsfAlgorithm: ED448", + "signature": { + "algorithm": "Ed448", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:ES256", + "name": "JsfAlgorithm: ES256", + "signature": { + "algorithm": "ES256", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:ES384", + "name": "JsfAlgorithm: ES384", + "signature": { + "algorithm": "ES384", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:ES512", + "name": "JsfAlgorithm: ES512", + "signature": { + "algorithm": "ES512", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:HS256", + "name": "JsfAlgorithm: HS256", + "signature": { + "algorithm": "HS256", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:HS384", + "name": "JsfAlgorithm: HS384", + "signature": { + "algorithm": "HS384", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:HS512", + "name": "JsfAlgorithm: HS512", + "signature": { + "algorithm": "HS512", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:PS256", + "name": "JsfAlgorithm: PS256", + "signature": { + "algorithm": "PS256", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:PS384", + "name": "JsfAlgorithm: PS384", + "signature": { + "algorithm": "PS384", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:PS512", + "name": "JsfAlgorithm: PS512", + "signature": { + "algorithm": "PS512", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:RS256", + "name": "JsfAlgorithm: RS256", + "signature": { + "algorithm": "RS256", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:RS384", + "name": "JsfAlgorithm: RS384", + "signature": { + "algorithm": "RS384", + "value": "AABBCC==" + }, + "type": "library" + }, + { + "bom-ref": "dummy-JA:RS512", + "name": "JsfAlgorithm: RS512", + "signature": { + "algorithm": "RS512", + "value": "AABBCC==" + }, + "type": "library" + } + ], + "dependencies": [ + { + "ref": "dummy-JA:ED25519" + }, + { + "ref": "dummy-JA:ED448" + }, + { + "ref": "dummy-JA:ES256" + }, + { + "ref": "dummy-JA:ES384" + }, + { + "ref": "dummy-JA:ES512" + }, + { + "ref": "dummy-JA:HS256" + }, + { + "ref": "dummy-JA:HS384" + }, + { + "ref": "dummy-JA:HS512" + }, + { + "ref": "dummy-JA:PS256" + }, + { + "ref": "dummy-JA:PS384" + }, + { + "ref": "dummy-JA:PS512" + }, + { + "ref": "dummy-JA:RS256" + }, + { + "ref": "dummy-JA:RS384" + }, + { + "ref": "dummy-JA:RS512" + } + ], + "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_JsfEcCurve-1.4.json.bin b/tests/_data/snapshots/enum_JsfEcCurve-1.4.json.bin new file mode 100644 index 00000000..3dd9381c --- /dev/null +++ b/tests/_data/snapshots/enum_JsfEcCurve-1.4.json.bin @@ -0,0 +1,68 @@ +{ + "components": [ + { + "bom-ref": "dummy-JEC:P_256", + "name": "JsfEcCurve: P_256", + "signature": { + "algorithm": "ES256", + "value": "AABBCC==", + "publicKey": { + "kty": "EC", + "crv": "P-256", + "x": "abc", + "y": "def" + } + }, + "type": "library" + }, + { + "bom-ref": "dummy-JEC:P_384", + "name": "JsfEcCurve: P_384", + "signature": { + "algorithm": "ES256", + "value": "AABBCC==", + "publicKey": { + "kty": "EC", + "crv": "P-384", + "x": "abc", + "y": "def" + } + }, + "type": "library" + }, + { + "bom-ref": "dummy-JEC:P_521", + "name": "JsfEcCurve: P_521", + "signature": { + "algorithm": "ES256", + "value": "AABBCC==", + "publicKey": { + "kty": "EC", + "crv": "P-521", + "x": "abc", + "y": "def" + } + }, + "type": "library" + } + ], + "dependencies": [ + { + "ref": "dummy-JEC:P_256" + }, + { + "ref": "dummy-JEC:P_384" + }, + { + "ref": "dummy-JEC:P_521" + } + ], + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.4" +} \ No newline at end of file diff --git a/tests/_data/snapshots/enum_JsfEcCurve-1.5.json.bin b/tests/_data/snapshots/enum_JsfEcCurve-1.5.json.bin new file mode 100644 index 00000000..945f6926 --- /dev/null +++ b/tests/_data/snapshots/enum_JsfEcCurve-1.5.json.bin @@ -0,0 +1,78 @@ +{ + "components": [ + { + "bom-ref": "dummy-JEC:P_256", + "name": "JsfEcCurve: P_256", + "signature": { + "algorithm": "ES256", + "value": "AABBCC==", + "publicKey": { + "kty": "EC", + "crv": "P-256", + "x": "abc", + "y": "def" + } + }, + "type": "library" + }, + { + "bom-ref": "dummy-JEC:P_384", + "name": "JsfEcCurve: P_384", + "signature": { + "algorithm": "ES256", + "value": "AABBCC==", + "publicKey": { + "kty": "EC", + "crv": "P-384", + "x": "abc", + "y": "def" + } + }, + "type": "library" + }, + { + "bom-ref": "dummy-JEC:P_521", + "name": "JsfEcCurve: P_521", + "signature": { + "algorithm": "ES256", + "value": "AABBCC==", + "publicKey": { + "kty": "EC", + "crv": "P-521", + "x": "abc", + "y": "def" + } + }, + "type": "library" + } + ], + "dependencies": [ + { + "ref": "dummy-JEC:P_256" + }, + { + "ref": "dummy-JEC:P_384" + }, + { + "ref": "dummy-JEC:P_521" + } + ], + "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.5.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.5" +} \ No newline at end of file diff --git a/tests/_data/snapshots/enum_JsfEcCurve-1.6.json.bin b/tests/_data/snapshots/enum_JsfEcCurve-1.6.json.bin new file mode 100644 index 00000000..8ab8e8fd --- /dev/null +++ b/tests/_data/snapshots/enum_JsfEcCurve-1.6.json.bin @@ -0,0 +1,78 @@ +{ + "components": [ + { + "bom-ref": "dummy-JEC:P_256", + "name": "JsfEcCurve: P_256", + "signature": { + "algorithm": "ES256", + "value": "AABBCC==", + "publicKey": { + "kty": "EC", + "crv": "P-256", + "x": "abc", + "y": "def" + } + }, + "type": "library" + }, + { + "bom-ref": "dummy-JEC:P_384", + "name": "JsfEcCurve: P_384", + "signature": { + "algorithm": "ES256", + "value": "AABBCC==", + "publicKey": { + "kty": "EC", + "crv": "P-384", + "x": "abc", + "y": "def" + } + }, + "type": "library" + }, + { + "bom-ref": "dummy-JEC:P_521", + "name": "JsfEcCurve: P_521", + "signature": { + "algorithm": "ES256", + "value": "AABBCC==", + "publicKey": { + "kty": "EC", + "crv": "P-521", + "x": "abc", + "y": "def" + } + }, + "type": "library" + } + ], + "dependencies": [ + { + "ref": "dummy-JEC:P_256" + }, + { + "ref": "dummy-JEC:P_384" + }, + { + "ref": "dummy-JEC:P_521" + } + ], + "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.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6" +} \ No newline at end of file diff --git a/tests/_data/snapshots/enum_JsfEcCurve-1.7.json.bin b/tests/_data/snapshots/enum_JsfEcCurve-1.7.json.bin new file mode 100644 index 00000000..0c05eac3 --- /dev/null +++ b/tests/_data/snapshots/enum_JsfEcCurve-1.7.json.bin @@ -0,0 +1,78 @@ +{ + "components": [ + { + "bom-ref": "dummy-JEC:P_256", + "name": "JsfEcCurve: P_256", + "signature": { + "algorithm": "ES256", + "value": "AABBCC==", + "publicKey": { + "kty": "EC", + "crv": "P-256", + "x": "abc", + "y": "def" + } + }, + "type": "library" + }, + { + "bom-ref": "dummy-JEC:P_384", + "name": "JsfEcCurve: P_384", + "signature": { + "algorithm": "ES256", + "value": "AABBCC==", + "publicKey": { + "kty": "EC", + "crv": "P-384", + "x": "abc", + "y": "def" + } + }, + "type": "library" + }, + { + "bom-ref": "dummy-JEC:P_521", + "name": "JsfEcCurve: P_521", + "signature": { + "algorithm": "ES256", + "value": "AABBCC==", + "publicKey": { + "kty": "EC", + "crv": "P-521", + "x": "abc", + "y": "def" + } + }, + "type": "library" + } + ], + "dependencies": [ + { + "ref": "dummy-JEC:P_256" + }, + { + "ref": "dummy-JEC:P_384" + }, + { + "ref": "dummy-JEC:P_521" + } + ], + "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_JsfKeyType-1.4.json.bin b/tests/_data/snapshots/enum_JsfKeyType-1.4.json.bin new file mode 100644 index 00000000..28f6621c --- /dev/null +++ b/tests/_data/snapshots/enum_JsfKeyType-1.4.json.bin @@ -0,0 +1,66 @@ +{ + "components": [ + { + "bom-ref": "dummy-JKT:EC", + "name": "JsfKeyType: EC", + "signature": { + "algorithm": "ES256", + "value": "DDEEFF==", + "publicKey": { + "kty": "EC", + "crv": "P-256", + "x": "abc", + "y": "def" + } + }, + "type": "library" + }, + { + "bom-ref": "dummy-JKT:OKP", + "name": "JsfKeyType: OKP", + "signature": { + "algorithm": "Ed25519", + "value": "GGHHII==", + "publicKey": { + "kty": "OKP", + "crv": "Ed25519", + "x": "xyz" + } + }, + "type": "library" + }, + { + "bom-ref": "dummy-JKT:RSA", + "name": "JsfKeyType: RSA", + "signature": { + "algorithm": "RS256", + "value": "AABBCC==", + "publicKey": { + "kty": "RSA", + "n": "sQ3MDBw==", + "e": "AQAB" + } + }, + "type": "library" + } + ], + "dependencies": [ + { + "ref": "dummy-JKT:EC" + }, + { + "ref": "dummy-JKT:OKP" + }, + { + "ref": "dummy-JKT:RSA" + } + ], + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.4" +} \ No newline at end of file diff --git a/tests/_data/snapshots/enum_JsfKeyType-1.5.json.bin b/tests/_data/snapshots/enum_JsfKeyType-1.5.json.bin new file mode 100644 index 00000000..650de4c5 --- /dev/null +++ b/tests/_data/snapshots/enum_JsfKeyType-1.5.json.bin @@ -0,0 +1,76 @@ +{ + "components": [ + { + "bom-ref": "dummy-JKT:EC", + "name": "JsfKeyType: EC", + "signature": { + "algorithm": "ES256", + "value": "DDEEFF==", + "publicKey": { + "kty": "EC", + "crv": "P-256", + "x": "abc", + "y": "def" + } + }, + "type": "library" + }, + { + "bom-ref": "dummy-JKT:OKP", + "name": "JsfKeyType: OKP", + "signature": { + "algorithm": "Ed25519", + "value": "GGHHII==", + "publicKey": { + "kty": "OKP", + "crv": "Ed25519", + "x": "xyz" + } + }, + "type": "library" + }, + { + "bom-ref": "dummy-JKT:RSA", + "name": "JsfKeyType: RSA", + "signature": { + "algorithm": "RS256", + "value": "AABBCC==", + "publicKey": { + "kty": "RSA", + "n": "sQ3MDBw==", + "e": "AQAB" + } + }, + "type": "library" + } + ], + "dependencies": [ + { + "ref": "dummy-JKT:EC" + }, + { + "ref": "dummy-JKT:OKP" + }, + { + "ref": "dummy-JKT:RSA" + } + ], + "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.5.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.5" +} \ No newline at end of file diff --git a/tests/_data/snapshots/enum_JsfKeyType-1.6.json.bin b/tests/_data/snapshots/enum_JsfKeyType-1.6.json.bin new file mode 100644 index 00000000..c9d31b19 --- /dev/null +++ b/tests/_data/snapshots/enum_JsfKeyType-1.6.json.bin @@ -0,0 +1,76 @@ +{ + "components": [ + { + "bom-ref": "dummy-JKT:EC", + "name": "JsfKeyType: EC", + "signature": { + "algorithm": "ES256", + "value": "DDEEFF==", + "publicKey": { + "kty": "EC", + "crv": "P-256", + "x": "abc", + "y": "def" + } + }, + "type": "library" + }, + { + "bom-ref": "dummy-JKT:OKP", + "name": "JsfKeyType: OKP", + "signature": { + "algorithm": "Ed25519", + "value": "GGHHII==", + "publicKey": { + "kty": "OKP", + "crv": "Ed25519", + "x": "xyz" + } + }, + "type": "library" + }, + { + "bom-ref": "dummy-JKT:RSA", + "name": "JsfKeyType: RSA", + "signature": { + "algorithm": "RS256", + "value": "AABBCC==", + "publicKey": { + "kty": "RSA", + "n": "sQ3MDBw==", + "e": "AQAB" + } + }, + "type": "library" + } + ], + "dependencies": [ + { + "ref": "dummy-JKT:EC" + }, + { + "ref": "dummy-JKT:OKP" + }, + { + "ref": "dummy-JKT:RSA" + } + ], + "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.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6" +} \ No newline at end of file diff --git a/tests/_data/snapshots/enum_JsfKeyType-1.7.json.bin b/tests/_data/snapshots/enum_JsfKeyType-1.7.json.bin new file mode 100644 index 00000000..9861ee08 --- /dev/null +++ b/tests/_data/snapshots/enum_JsfKeyType-1.7.json.bin @@ -0,0 +1,76 @@ +{ + "components": [ + { + "bom-ref": "dummy-JKT:EC", + "name": "JsfKeyType: EC", + "signature": { + "algorithm": "ES256", + "value": "DDEEFF==", + "publicKey": { + "kty": "EC", + "crv": "P-256", + "x": "abc", + "y": "def" + } + }, + "type": "library" + }, + { + "bom-ref": "dummy-JKT:OKP", + "name": "JsfKeyType: OKP", + "signature": { + "algorithm": "Ed25519", + "value": "GGHHII==", + "publicKey": { + "kty": "OKP", + "crv": "Ed25519", + "x": "xyz" + } + }, + "type": "library" + }, + { + "bom-ref": "dummy-JKT:RSA", + "name": "JsfKeyType: RSA", + "signature": { + "algorithm": "RS256", + "value": "AABBCC==", + "publicKey": { + "kty": "RSA", + "n": "sQ3MDBw==", + "e": "AQAB" + } + }, + "type": "library" + } + ], + "dependencies": [ + { + "ref": "dummy-JKT:EC" + }, + { + "ref": "dummy-JKT:OKP" + }, + { + "ref": "dummy-JKT:RSA" + } + ], + "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_JsfOkpCurve-1.4.json.bin b/tests/_data/snapshots/enum_JsfOkpCurve-1.4.json.bin new file mode 100644 index 00000000..32afba81 --- /dev/null +++ b/tests/_data/snapshots/enum_JsfOkpCurve-1.4.json.bin @@ -0,0 +1,48 @@ +{ + "components": [ + { + "bom-ref": "dummy-JOC:ED25519", + "name": "JsfOkpCurve: ED25519", + "signature": { + "algorithm": "Ed25519", + "value": "AABBCC==", + "publicKey": { + "kty": "OKP", + "crv": "Ed25519", + "x": "xyz" + } + }, + "type": "library" + }, + { + "bom-ref": "dummy-JOC:ED448", + "name": "JsfOkpCurve: ED448", + "signature": { + "algorithm": "Ed25519", + "value": "AABBCC==", + "publicKey": { + "kty": "OKP", + "crv": "Ed448", + "x": "xyz" + } + }, + "type": "library" + } + ], + "dependencies": [ + { + "ref": "dummy-JOC:ED25519" + }, + { + "ref": "dummy-JOC:ED448" + } + ], + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.4" +} \ No newline at end of file diff --git a/tests/_data/snapshots/enum_JsfOkpCurve-1.5.json.bin b/tests/_data/snapshots/enum_JsfOkpCurve-1.5.json.bin new file mode 100644 index 00000000..1807fff6 --- /dev/null +++ b/tests/_data/snapshots/enum_JsfOkpCurve-1.5.json.bin @@ -0,0 +1,58 @@ +{ + "components": [ + { + "bom-ref": "dummy-JOC:ED25519", + "name": "JsfOkpCurve: ED25519", + "signature": { + "algorithm": "Ed25519", + "value": "AABBCC==", + "publicKey": { + "kty": "OKP", + "crv": "Ed25519", + "x": "xyz" + } + }, + "type": "library" + }, + { + "bom-ref": "dummy-JOC:ED448", + "name": "JsfOkpCurve: ED448", + "signature": { + "algorithm": "Ed25519", + "value": "AABBCC==", + "publicKey": { + "kty": "OKP", + "crv": "Ed448", + "x": "xyz" + } + }, + "type": "library" + } + ], + "dependencies": [ + { + "ref": "dummy-JOC:ED25519" + }, + { + "ref": "dummy-JOC:ED448" + } + ], + "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.5.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.5" +} \ No newline at end of file diff --git a/tests/_data/snapshots/enum_JsfOkpCurve-1.6.json.bin b/tests/_data/snapshots/enum_JsfOkpCurve-1.6.json.bin new file mode 100644 index 00000000..f5deae3d --- /dev/null +++ b/tests/_data/snapshots/enum_JsfOkpCurve-1.6.json.bin @@ -0,0 +1,58 @@ +{ + "components": [ + { + "bom-ref": "dummy-JOC:ED25519", + "name": "JsfOkpCurve: ED25519", + "signature": { + "algorithm": "Ed25519", + "value": "AABBCC==", + "publicKey": { + "kty": "OKP", + "crv": "Ed25519", + "x": "xyz" + } + }, + "type": "library" + }, + { + "bom-ref": "dummy-JOC:ED448", + "name": "JsfOkpCurve: ED448", + "signature": { + "algorithm": "Ed25519", + "value": "AABBCC==", + "publicKey": { + "kty": "OKP", + "crv": "Ed448", + "x": "xyz" + } + }, + "type": "library" + } + ], + "dependencies": [ + { + "ref": "dummy-JOC:ED25519" + }, + { + "ref": "dummy-JOC:ED448" + } + ], + "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.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6" +} \ No newline at end of file diff --git a/tests/_data/snapshots/enum_JsfOkpCurve-1.7.json.bin b/tests/_data/snapshots/enum_JsfOkpCurve-1.7.json.bin new file mode 100644 index 00000000..e1f2e049 --- /dev/null +++ b/tests/_data/snapshots/enum_JsfOkpCurve-1.7.json.bin @@ -0,0 +1,58 @@ +{ + "components": [ + { + "bom-ref": "dummy-JOC:ED25519", + "name": "JsfOkpCurve: ED25519", + "signature": { + "algorithm": "Ed25519", + "value": "AABBCC==", + "publicKey": { + "kty": "OKP", + "crv": "Ed25519", + "x": "xyz" + } + }, + "type": "library" + }, + { + "bom-ref": "dummy-JOC:ED448", + "name": "JsfOkpCurve: ED448", + "signature": { + "algorithm": "Ed25519", + "value": "AABBCC==", + "publicKey": { + "kty": "OKP", + "crv": "Ed448", + "x": "xyz" + } + }, + "type": "library" + } + ], + "dependencies": [ + { + "ref": "dummy-JOC:ED25519" + }, + { + "ref": "dummy-JOC:ED448" + } + ], + "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/get_bom_with_signatures-1.0.xml.bin b/tests/_data/snapshots/get_bom_with_signatures-1.0.xml.bin new file mode 100644 index 00000000..ac13323f --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_signatures-1.0.xml.bin @@ -0,0 +1,10 @@ + + + + + acme-library + 1.2.3 + false + + + diff --git a/tests/_data/snapshots/get_bom_with_signatures-1.1.xml.bin b/tests/_data/snapshots/get_bom_with_signatures-1.1.xml.bin new file mode 100644 index 00000000..c5309b87 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_signatures-1.1.xml.bin @@ -0,0 +1,9 @@ + + + + + acme-library + 1.2.3 + + + diff --git a/tests/_data/snapshots/get_bom_with_signatures-1.2.json.bin b/tests/_data/snapshots/get_bom_with_signatures-1.2.json.bin new file mode 100644 index 00000000..25c49392 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_signatures-1.2.json.bin @@ -0,0 +1,41 @@ +{ + "components": [ + { + "bom-ref": "acme-library", + "name": "acme-library", + "type": "library", + "version": "1.2.3" + } + ], + "dependencies": [ + { + "ref": "acme-library" + }, + { + "ref": "acme-service" + }, + { + "ref": "my-app" + } + ], + "metadata": { + "component": { + "bom-ref": "my-app", + "name": "my-app", + "type": "application", + "version": "0.1.0" + }, + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "services": [ + { + "bom-ref": "acme-service", + "name": "acme-service" + } + ], + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.2b.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.2" +} \ No newline at end of file diff --git a/tests/_data/snapshots/get_bom_with_signatures-1.2.xml.bin b/tests/_data/snapshots/get_bom_with_signatures-1.2.xml.bin new file mode 100644 index 00000000..a3ce5ef5 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_signatures-1.2.xml.bin @@ -0,0 +1,26 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + my-app + 0.1.0 + + + + + acme-library + 1.2.3 + + + + + acme-service + + + + + + + + diff --git a/tests/_data/snapshots/get_bom_with_signatures-1.3.json.bin b/tests/_data/snapshots/get_bom_with_signatures-1.3.json.bin new file mode 100644 index 00000000..244d70de --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_signatures-1.3.json.bin @@ -0,0 +1,41 @@ +{ + "components": [ + { + "bom-ref": "acme-library", + "name": "acme-library", + "type": "library", + "version": "1.2.3" + } + ], + "dependencies": [ + { + "ref": "acme-library" + }, + { + "ref": "acme-service" + }, + { + "ref": "my-app" + } + ], + "metadata": { + "component": { + "bom-ref": "my-app", + "name": "my-app", + "type": "application", + "version": "0.1.0" + }, + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "services": [ + { + "bom-ref": "acme-service", + "name": "acme-service" + } + ], + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.3a.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.3" +} \ No newline at end of file diff --git a/tests/_data/snapshots/get_bom_with_signatures-1.3.xml.bin b/tests/_data/snapshots/get_bom_with_signatures-1.3.xml.bin new file mode 100644 index 00000000..26d23b43 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_signatures-1.3.xml.bin @@ -0,0 +1,26 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + my-app + 0.1.0 + + + + + acme-library + 1.2.3 + + + + + acme-service + + + + + + + + diff --git a/tests/_data/snapshots/get_bom_with_signatures-1.4.json.bin b/tests/_data/snapshots/get_bom_with_signatures-1.4.json.bin new file mode 100644 index 00000000..8c862572 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_signatures-1.4.json.bin @@ -0,0 +1,77 @@ +{ + "components": [ + { + "bom-ref": "acme-library", + "name": "acme-library", + "signature": { + "algorithm": "ES256", + "value": "MEQCIAJ9DECTPNuqwdWHlHO3EB1jYVnjW7HZ0T7x3QIDO4OMfAIgTFz5kl3Zl7nBP4r2TovMnbJo3ij6JTANcFAQQVEBZQ==", + "keyId": "test-key-1" + }, + "type": "library", + "version": "1.2.3" + } + ], + "dependencies": [ + { + "ref": "acme-library" + }, + { + "ref": "acme-service" + }, + { + "ref": "my-app" + } + ], + "metadata": { + "component": { + "bom-ref": "my-app", + "name": "my-app", + "type": "application", + "version": "0.1.0" + }, + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "services": [ + { + "bom-ref": "acme-service", + "name": "acme-service", + "signature": { + "chain": [ + { + "algorithm": "Ed25519", + "value": "xyzSig==" + } + ] + } + } + ], + "signature": { + "signers": [ + { + "algorithm": "RS256", + "value": "AABBCC==", + "publicKey": { + "kty": "RSA", + "n": "sQ3MDBw==", + "e": "AQAB" + } + }, + { + "algorithm": "ES384", + "value": "DDEEFF==", + "certificatePath": [ + "MIICpDCCAYwCCQDU" + ], + "excludes": [ + "signature" + ] + } + ] + }, + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.4" +} \ No newline at end of file diff --git a/tests/_data/snapshots/get_bom_with_signatures-1.4.xml.bin b/tests/_data/snapshots/get_bom_with_signatures-1.4.xml.bin new file mode 100644 index 00000000..076ffd52 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_signatures-1.4.xml.bin @@ -0,0 +1,26 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + my-app + 0.1.0 + + + + + acme-library + 1.2.3 + + + + + acme-service + + + + + + + + diff --git a/tests/_data/snapshots/get_bom_with_signatures-1.5.json.bin b/tests/_data/snapshots/get_bom_with_signatures-1.5.json.bin new file mode 100644 index 00000000..0bc665de --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_signatures-1.5.json.bin @@ -0,0 +1,87 @@ +{ + "components": [ + { + "bom-ref": "acme-library", + "name": "acme-library", + "signature": { + "algorithm": "ES256", + "value": "MEQCIAJ9DECTPNuqwdWHlHO3EB1jYVnjW7HZ0T7x3QIDO4OMfAIgTFz5kl3Zl7nBP4r2TovMnbJo3ij6JTANcFAQQVEBZQ==", + "keyId": "test-key-1" + }, + "type": "library", + "version": "1.2.3" + } + ], + "dependencies": [ + { + "ref": "acme-library" + }, + { + "ref": "acme-service" + }, + { + "ref": "my-app" + } + ], + "metadata": { + "component": { + "bom-ref": "my-app", + "name": "my-app", + "type": "application", + "version": "0.1.0" + }, + "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", + "services": [ + { + "bom-ref": "acme-service", + "name": "acme-service", + "signature": { + "chain": [ + { + "algorithm": "Ed25519", + "value": "xyzSig==" + } + ] + } + } + ], + "signature": { + "signers": [ + { + "algorithm": "RS256", + "value": "AABBCC==", + "publicKey": { + "kty": "RSA", + "n": "sQ3MDBw==", + "e": "AQAB" + } + }, + { + "algorithm": "ES384", + "value": "DDEEFF==", + "certificatePath": [ + "MIICpDCCAYwCCQDU" + ], + "excludes": [ + "signature" + ] + } + ] + }, + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.5" +} \ No newline at end of file diff --git a/tests/_data/snapshots/get_bom_with_signatures-1.5.xml.bin b/tests/_data/snapshots/get_bom_with_signatures-1.5.xml.bin new file mode 100644 index 00000000..eef873be --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_signatures-1.5.xml.bin @@ -0,0 +1,30 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + my-app + 0.1.0 + + + + + acme-library + 1.2.3 + + + + + acme-service + + + + + + + + + val1 + val2 + + diff --git a/tests/_data/snapshots/get_bom_with_signatures-1.6.json.bin b/tests/_data/snapshots/get_bom_with_signatures-1.6.json.bin new file mode 100644 index 00000000..d67f7871 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_signatures-1.6.json.bin @@ -0,0 +1,87 @@ +{ + "components": [ + { + "bom-ref": "acme-library", + "name": "acme-library", + "signature": { + "algorithm": "ES256", + "value": "MEQCIAJ9DECTPNuqwdWHlHO3EB1jYVnjW7HZ0T7x3QIDO4OMfAIgTFz5kl3Zl7nBP4r2TovMnbJo3ij6JTANcFAQQVEBZQ==", + "keyId": "test-key-1" + }, + "type": "library", + "version": "1.2.3" + } + ], + "dependencies": [ + { + "ref": "acme-library" + }, + { + "ref": "acme-service" + }, + { + "ref": "my-app" + } + ], + "metadata": { + "component": { + "bom-ref": "my-app", + "name": "my-app", + "type": "application", + "version": "0.1.0" + }, + "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", + "services": [ + { + "bom-ref": "acme-service", + "name": "acme-service", + "signature": { + "chain": [ + { + "algorithm": "Ed25519", + "value": "xyzSig==" + } + ] + } + } + ], + "signature": { + "signers": [ + { + "algorithm": "RS256", + "value": "AABBCC==", + "publicKey": { + "kty": "RSA", + "n": "sQ3MDBw==", + "e": "AQAB" + } + }, + { + "algorithm": "ES384", + "value": "DDEEFF==", + "certificatePath": [ + "MIICpDCCAYwCCQDU" + ], + "excludes": [ + "signature" + ] + } + ] + }, + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6" +} \ No newline at end of file diff --git a/tests/_data/snapshots/get_bom_with_signatures-1.6.xml.bin b/tests/_data/snapshots/get_bom_with_signatures-1.6.xml.bin new file mode 100644 index 00000000..6a8e7f3a --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_signatures-1.6.xml.bin @@ -0,0 +1,30 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + my-app + 0.1.0 + + + + + acme-library + 1.2.3 + + + + + acme-service + + + + + + + + + val1 + val2 + + diff --git a/tests/_data/snapshots/get_bom_with_signatures-1.7.json.bin b/tests/_data/snapshots/get_bom_with_signatures-1.7.json.bin new file mode 100644 index 00000000..a0751be5 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_signatures-1.7.json.bin @@ -0,0 +1,87 @@ +{ + "components": [ + { + "bom-ref": "acme-library", + "name": "acme-library", + "signature": { + "algorithm": "ES256", + "value": "MEQCIAJ9DECTPNuqwdWHlHO3EB1jYVnjW7HZ0T7x3QIDO4OMfAIgTFz5kl3Zl7nBP4r2TovMnbJo3ij6JTANcFAQQVEBZQ==", + "keyId": "test-key-1" + }, + "type": "library", + "version": "1.2.3" + } + ], + "dependencies": [ + { + "ref": "acme-library" + }, + { + "ref": "acme-service" + }, + { + "ref": "my-app" + } + ], + "metadata": { + "component": { + "bom-ref": "my-app", + "name": "my-app", + "type": "application", + "version": "0.1.0" + }, + "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", + "services": [ + { + "bom-ref": "acme-service", + "name": "acme-service", + "signature": { + "chain": [ + { + "algorithm": "Ed25519", + "value": "xyzSig==" + } + ] + } + } + ], + "signature": { + "signers": [ + { + "algorithm": "RS256", + "value": "AABBCC==", + "publicKey": { + "kty": "RSA", + "n": "sQ3MDBw==", + "e": "AQAB" + } + }, + { + "algorithm": "ES384", + "value": "DDEEFF==", + "certificatePath": [ + "MIICpDCCAYwCCQDU" + ], + "excludes": [ + "signature" + ] + } + ] + }, + "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/get_bom_with_signatures-1.7.xml.bin b/tests/_data/snapshots/get_bom_with_signatures-1.7.xml.bin new file mode 100644 index 00000000..1fbcdd14 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_signatures-1.7.xml.bin @@ -0,0 +1,30 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + my-app + 0.1.0 + + + + + acme-library + 1.2.3 + + + + + acme-service + + + + + + + + + val1 + val2 + + diff --git a/tests/test_deserialize_xml.py b/tests/test_deserialize_xml.py index 8ad0f31c..64be7c35 100644 --- a/tests/test_deserialize_xml.py +++ b/tests/test_deserialize_xml.py @@ -27,6 +27,7 @@ from cyclonedx.schema import OutputFormat, SchemaVersion from tests import OWN_DATA_DIRECTORY, DeepCompareMixin, SnapshotMixin, mksname from tests._data.models import ( + all_get_bom_funct_no_xml_roundtrip_immut, all_get_bom_funct_valid_immut, all_get_bom_funct_valid_reversible_migrate, all_get_bom_funct_with_incomplete_deps, @@ -50,6 +51,15 @@ def test_prepared(self, get_bom: Callable[[], Bom], *_: Any, **__: Any) -> None: self.assertBomDeepEqual(expected, bom, fuzzy_deps=get_bom in all_get_bom_funct_with_incomplete_deps) + @named_data(*all_get_bom_funct_no_xml_roundtrip_immut) + @patch('cyclonedx.contrib.this.builders.__ThisVersion', 'TESTING') + def test_no_xml_roundtrip(self, get_bom: Callable[[], Bom], *_: Any, **__: Any) -> None: + # JSON-only fields (e.g. JSF signatures) are silently dropped during XML serialization. + # Verify only that deserialization of the XML snapshot succeeds without error. + snapshot_name = mksname(get_bom, _LATEST_SCHEMA, OutputFormat.XML) + with open(self.getSnapshotFile(snapshot_name)) as s: + Bom.from_xml(s) + def test_component_evidence_identity(self) -> None: xml_file = join(OWN_DATA_DIRECTORY, 'xml', SchemaVersion.V1_6.to_version(), diff --git a/tests/test_enums.py b/tests/test_enums.py index 88ac8e71..c33695d8 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -46,6 +46,7 @@ from cyclonedx.model.license import DisjunctiveLicense from cyclonedx.model.lifecycle import LifecyclePhase, PredefinedLifecycle from cyclonedx.model.service import DataClassification, Service +from cyclonedx.model.signature import JsfPublicKey, JsfSimpleSignature from cyclonedx.model.vulnerability import ( BomTarget, BomTargetVersionRange, @@ -55,7 +56,7 @@ ) from cyclonedx.output import make_outputter from cyclonedx.schema import OutputFormat, SchemaVersion -from cyclonedx.schema._res import BOM_JSON as SCHEMA_JSON, BOM_XML as SCHEMA_XML +from cyclonedx.schema._res import BOM_JSON as SCHEMA_JSON, BOM_XML as SCHEMA_XML, JSF as SCHEMA_JSF from cyclonedx.validation import make_schemabased_validator from tests import PROJECT_LIB_MODELS_DIRECTORY, SnapshotMixin from tests._data.models import _make_bom @@ -106,6 +107,12 @@ RelatedCryptoMaterialState, RelatedCryptoMaterialType, ) +from cyclonedx.model.signature import ( # isort:skip + JsfAlgorithm, + JsfEcCurve, + JsfKeyType, + JsfOkpCurve, +) # endregion SUT @@ -153,6 +160,30 @@ def dp_cases_from_json_schemas(*jsonpointer: str) -> set[str]: return cases +def dp_cases_from_jsf_algorithm() -> set[str]: + with open(SCHEMA_JSF) as sfh: + data = json_load(sfh) + return set(data['definitions']['signer']['properties']['algorithm']['oneOf'][0]['enum']) + + +def dp_cases_from_jsf_keytype() -> set[str]: + with open(SCHEMA_JSF) as sfh: + data = json_load(sfh) + return set(data['definitions']['keyType']['enum']) + + +def dp_cases_from_jsf_ec_crv() -> set[str]: + with open(SCHEMA_JSF) as sfh: + data = json_load(sfh) + return set(data['definitions']['publicKey']['allOf'][0]['then']['properties']['crv']['enum']) + + +def dp_cases_from_jsf_okp_crv() -> set[str]: + with open(SCHEMA_JSF) as sfh: + data = json_load(sfh) + return set(data['definitions']['publicKey']['allOf'][1]['then']['properties']['crv']['enum']) + + UNSUPPORTED_OF_SV = frozenset([ (OutputFormat.JSON, SchemaVersion.V1_1), (OutputFormat.JSON, SchemaVersion.V1_0), @@ -977,6 +1008,119 @@ def test_cases_render_valid(self, of: OutputFormat, sv: SchemaVersion, *_: Any, super()._test_cases_render(bom, of, sv) +@ddt +class TestEnumJsfAlgorithm(_EnumTestCase): + + @idata(dp_cases_from_jsf_algorithm()) + def test_knows_value(self, value: str) -> None: + super()._test_knows_value(JsfAlgorithm, value) + + @named_data(*(d for d in NAMED_OF_SV if d[1] == OutputFormat.JSON and d[2] >= SchemaVersion.V1_4)) + def test_cases_render_valid(self, of: OutputFormat, sv: SchemaVersion, *_: Any, **__: Any) -> None: + bom = _make_bom( + components=[ + Component( + name=f'JsfAlgorithm: {alg.name}', bom_ref=f'dummy-JA:{alg.name}', + type=ComponentType.LIBRARY, + signature=JsfSimpleSignature( + algorithm=alg, + value='AABBCC==', + ), + ) for alg in JsfAlgorithm + ]) + super()._test_cases_render(bom, of, sv) + + +@ddt +class TestEnumJsfKeyType(_EnumTestCase): + + @idata(dp_cases_from_jsf_keytype()) + def test_knows_value(self, value: str) -> None: + super()._test_knows_value(JsfKeyType, value) + + @named_data(*(d for d in NAMED_OF_SV if d[1] == OutputFormat.JSON and d[2] >= SchemaVersion.V1_4)) + def test_cases_render_valid(self, of: OutputFormat, sv: SchemaVersion, *_: Any, **__: Any) -> None: + bom = _make_bom( + components=[ + Component( + name='JsfKeyType: EC', bom_ref='dummy-JKT:EC', + type=ComponentType.LIBRARY, + signature=JsfSimpleSignature( + algorithm=JsfAlgorithm.ES256, + value='DDEEFF==', + public_key=JsfPublicKey(kty=JsfKeyType.EC, crv=JsfEcCurve.P_256, x='abc', y='def'), + ), + ), + Component( + name='JsfKeyType: OKP', bom_ref='dummy-JKT:OKP', + type=ComponentType.LIBRARY, + signature=JsfSimpleSignature( + algorithm=JsfAlgorithm.ED25519, + value='GGHHII==', + public_key=JsfPublicKey(kty=JsfKeyType.OKP, crv=JsfOkpCurve.ED25519, x='xyz'), + ), + ), + Component( + name='JsfKeyType: RSA', bom_ref='dummy-JKT:RSA', + type=ComponentType.LIBRARY, + signature=JsfSimpleSignature( + algorithm=JsfAlgorithm.RS256, + value='AABBCC==', + public_key=JsfPublicKey(kty=JsfKeyType.RSA, n='sQ3MDBw==', e='AQAB'), + ), + ), + ]) + super()._test_cases_render(bom, of, sv) + + +@ddt +class TestEnumJsfEcCurve(_EnumTestCase): + + @idata(dp_cases_from_jsf_ec_crv()) + def test_knows_value(self, value: str) -> None: + super()._test_knows_value(JsfEcCurve, value) + + @named_data(*(d for d in NAMED_OF_SV if d[1] == OutputFormat.JSON and d[2] >= SchemaVersion.V1_4)) + def test_cases_render_valid(self, of: OutputFormat, sv: SchemaVersion, *_: Any, **__: Any) -> None: + bom = _make_bom( + components=[ + Component( + name=f'JsfEcCurve: {crv.name}', bom_ref=f'dummy-JEC:{crv.name}', + type=ComponentType.LIBRARY, + signature=JsfSimpleSignature( + algorithm=JsfAlgorithm.ES256, + value='AABBCC==', + public_key=JsfPublicKey(kty=JsfKeyType.EC, crv=crv, x='abc', y='def'), + ), + ) for crv in JsfEcCurve + ]) + super()._test_cases_render(bom, of, sv) + + +@ddt +class TestEnumJsfOkpCurve(_EnumTestCase): + + @idata(dp_cases_from_jsf_okp_crv()) + def test_knows_value(self, value: str) -> None: + super()._test_knows_value(JsfOkpCurve, value) + + @named_data(*(d for d in NAMED_OF_SV if d[1] == OutputFormat.JSON and d[2] >= SchemaVersion.V1_4)) + def test_cases_render_valid(self, of: OutputFormat, sv: SchemaVersion, *_: Any, **__: Any) -> None: + bom = _make_bom( + components=[ + Component( + name=f'JsfOkpCurve: {crv.name}', bom_ref=f'dummy-JOC:{crv.name}', + type=ComponentType.LIBRARY, + signature=JsfSimpleSignature( + algorithm=JsfAlgorithm.ED25519, + value='AABBCC==', + public_key=JsfPublicKey(kty=JsfKeyType.OKP, crv=crv, x='xyz'), + ), + ) for crv in JsfOkpCurve + ]) + super()._test_cases_render(bom, of, sv) + + # add new test cases above this line diff --git a/tests/test_model_signature.py b/tests/test_model_signature.py new file mode 100644 index 00000000..45e28eae --- /dev/null +++ b/tests/test_model_signature.py @@ -0,0 +1,498 @@ +# This file is part of CycloneDX Python Library +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. + +from unittest import TestCase + +from cyclonedx.exception.model import InvalidValueException +from cyclonedx.model.signature import ( + JsfAlgorithm, + JsfEcCurve, + JsfKeyType, + JsfOkpCurve, + JsfPublicKey, + JsfSignature, + JsfSignatureChain, + JsfSignatureSigners, + JsfSimpleSignature, + _JsfSignatureSerializationHelper, +) + + +class TestJsfPublicKey(TestCase): + + def test_ec_public_key(self) -> None: + pk = JsfPublicKey(kty=JsfKeyType.EC, crv=JsfEcCurve.P_256, x='abc', y='def') + self.assertEqual(pk.kty, JsfKeyType.EC) + self.assertEqual(pk.crv, 'P-256') + self.assertEqual(pk.x, 'abc') + self.assertEqual(pk.y, 'def') + self.assertIsNone(pk.n) + self.assertIsNone(pk.e) + + def test_okp_public_key(self) -> None: + pk = JsfPublicKey(kty=JsfKeyType.OKP, crv=JsfOkpCurve.ED25519, x='xyz') + self.assertEqual(pk.kty, JsfKeyType.OKP) + self.assertEqual(pk.crv, 'Ed25519') + self.assertEqual(pk.x, 'xyz') + self.assertIsNone(pk.y) + self.assertIsNone(pk.n) + self.assertIsNone(pk.e) + + def test_rsa_public_key(self) -> None: + pk = JsfPublicKey(kty=JsfKeyType.RSA, n='modulus', e='exponent') + self.assertEqual(pk.kty, JsfKeyType.RSA) + self.assertEqual(pk.n, 'modulus') + self.assertEqual(pk.e, 'exponent') + self.assertIsNone(pk.crv) + self.assertIsNone(pk.x) + self.assertIsNone(pk.y) + + def test_equality(self) -> None: + pk1 = JsfPublicKey(kty=JsfKeyType.EC, crv=JsfEcCurve.P_256, x='abc', y='def') + pk2 = JsfPublicKey(kty=JsfKeyType.EC, crv=JsfEcCurve.P_256, x='abc', y='def') + pk3 = JsfPublicKey(kty=JsfKeyType.RSA, n='n', e='e') + self.assertEqual(pk1, pk2) + self.assertNotEqual(pk1, pk3) + + def test_hash_stable(self) -> None: + pk1 = JsfPublicKey(kty=JsfKeyType.EC, crv=JsfEcCurve.P_256, x='abc', y='def') + pk2 = JsfPublicKey(kty=JsfKeyType.EC, crv=JsfEcCurve.P_256, x='abc', y='def') + self.assertEqual(hash(pk1), hash(pk2)) + + def test_sorting(self) -> None: + pks = [ + JsfPublicKey(kty=JsfKeyType.RSA, n='n', e='e'), + JsfPublicKey(kty=JsfKeyType.EC, crv=JsfEcCurve.P_256, x='abc', y='def'), + JsfPublicKey(kty=JsfKeyType.OKP, crv=JsfOkpCurve.ED25519, x='xyz'), + ] + sorted_pks = sorted(pks) + self.assertEqual(len(sorted_pks), 3) + + def test_ec_validation_missing_crv(self) -> None: + with self.assertRaises(InvalidValueException) as cm: + JsfPublicKey(kty=JsfKeyType.EC, x='abc', y='def') + self.assertIn('EC', str(cm.exception)) + + def test_ec_validation_missing_x(self) -> None: + with self.assertRaises(InvalidValueException) as cm: + JsfPublicKey(kty=JsfKeyType.EC, crv=JsfEcCurve.P_256, y='def') + self.assertIn('EC', str(cm.exception)) + + def test_ec_validation_missing_y(self) -> None: + with self.assertRaises(InvalidValueException) as cm: + JsfPublicKey(kty=JsfKeyType.EC, crv=JsfEcCurve.P_256, x='abc') + self.assertIn('EC', str(cm.exception)) + + def test_okp_validation_missing_crv(self) -> None: + with self.assertRaises(InvalidValueException) as cm: + JsfPublicKey(kty=JsfKeyType.OKP, x='xyz') + self.assertIn('OKP', str(cm.exception)) + + def test_okp_validation_missing_x(self) -> None: + with self.assertRaises(InvalidValueException) as cm: + JsfPublicKey(kty=JsfKeyType.OKP, crv=JsfOkpCurve.ED25519) + self.assertIn('OKP', str(cm.exception)) + + def test_rsa_validation_missing_n(self) -> None: + with self.assertRaises(InvalidValueException) as cm: + JsfPublicKey(kty=JsfKeyType.RSA, e='exponent') + self.assertIn('RSA', str(cm.exception)) + + def test_rsa_validation_missing_e(self) -> None: + with self.assertRaises(InvalidValueException) as cm: + JsfPublicKey(kty=JsfKeyType.RSA, n='modulus') + self.assertIn('RSA', str(cm.exception)) + + def test_repr(self) -> None: + pk = JsfPublicKey(kty=JsfKeyType.EC, crv=JsfEcCurve.P_256, x='abc', y='def') + self.assertIn('JsfPublicKey', repr(pk)) + self.assertIn('EC', repr(pk)) + + def test_ec_crv_string_rejected(self) -> None: + with self.assertRaises(InvalidValueException): + JsfPublicKey(kty=JsfKeyType.EC, crv='P-256', x='abc', y='def') # type: ignore[arg-type] + + def test_okp_crv_string_rejected(self) -> None: + with self.assertRaises(InvalidValueException): + JsfPublicKey(kty=JsfKeyType.OKP, crv='Ed25519', x='xyz') # type: ignore[arg-type] + + def test_ec_crv_enum_accepted(self) -> None: + pk = JsfPublicKey(kty=JsfKeyType.EC, crv=JsfEcCurve.P_384, x='abc', y='def') + self.assertEqual(pk.crv, JsfEcCurve.P_384) + + def test_okp_crv_enum_accepted(self) -> None: + pk = JsfPublicKey(kty=JsfKeyType.OKP, crv=JsfOkpCurve.ED448, x='xyz') + self.assertEqual(pk.crv, JsfOkpCurve.ED448) + + def test_ec_crv_wrong_type_rejected(self) -> None: + with self.assertRaises(InvalidValueException): + JsfPublicKey(kty=JsfKeyType.EC, crv=JsfOkpCurve.ED25519, x='abc', y='def') # type: ignore[arg-type] + + def test_okp_crv_wrong_type_rejected(self) -> None: + with self.assertRaises(InvalidValueException): + JsfPublicKey(kty=JsfKeyType.OKP, crv=JsfEcCurve.P_256, x='xyz') # type: ignore[arg-type] + + def test_ec_validation_with_n_rejected(self) -> None: + with self.assertRaises(InvalidValueException) as cm: + JsfPublicKey(kty=JsfKeyType.EC, crv=JsfEcCurve.P_256, x='abc', y='def', n='modulus') + self.assertIn('EC', str(cm.exception)) + + def test_ec_validation_with_e_rejected(self) -> None: + with self.assertRaises(InvalidValueException) as cm: + JsfPublicKey(kty=JsfKeyType.EC, crv=JsfEcCurve.P_256, x='abc', y='def', e='exponent') + self.assertIn('EC', str(cm.exception)) + + def test_okp_validation_with_y_rejected(self) -> None: + with self.assertRaises(InvalidValueException) as cm: + JsfPublicKey(kty=JsfKeyType.OKP, crv=JsfOkpCurve.ED25519, x='xyz', y='not-valid') + self.assertIn('OKP', str(cm.exception)) + + def test_okp_validation_with_n_rejected(self) -> None: + with self.assertRaises(InvalidValueException) as cm: + JsfPublicKey(kty=JsfKeyType.OKP, crv=JsfOkpCurve.ED25519, x='xyz', n='modulus') + self.assertIn('OKP', str(cm.exception)) + + def test_okp_validation_with_e_rejected(self) -> None: + with self.assertRaises(InvalidValueException) as cm: + JsfPublicKey(kty=JsfKeyType.OKP, crv=JsfOkpCurve.ED25519, x='xyz', e='exponent') + self.assertIn('OKP', str(cm.exception)) + + def test_rsa_validation_with_crv_rejected(self) -> None: + with self.assertRaises(InvalidValueException) as cm: + JsfPublicKey(kty=JsfKeyType.RSA, n='modulus', e='exponent', crv=JsfEcCurve.P_256) + self.assertIn('RSA', str(cm.exception)) + + def test_rsa_validation_with_x_rejected(self) -> None: + with self.assertRaises(InvalidValueException) as cm: + JsfPublicKey(kty=JsfKeyType.RSA, n='modulus', e='exponent', x='abc') + self.assertIn('RSA', str(cm.exception)) + + def test_rsa_validation_with_y_rejected(self) -> None: + with self.assertRaises(InvalidValueException) as cm: + JsfPublicKey(kty=JsfKeyType.RSA, n='modulus', e='exponent', y='def') + self.assertIn('RSA', str(cm.exception)) + + def test_crv_serialized_as_string_value(self) -> None: + pk = JsfPublicKey(kty=JsfKeyType.EC, crv=JsfEcCurve.P_521, x='abc', y='def') + d = pk._as_dict() + self.assertEqual(d['crv'], 'P-521') + self.assertIsInstance(d['crv'], str) + self.assertNotIsInstance(d['crv'], JsfEcCurve) + + +class TestJsfSimpleSignature(TestCase): + """Tests for JsfSimpleSignature (simple signature mode).""" + + def test_minimal(self) -> None: + sig = JsfSimpleSignature(algorithm=JsfAlgorithm.ES256, value='sig-value') + self.assertEqual(sig.algorithm, JsfAlgorithm.ES256) + self.assertEqual(sig.value, 'sig-value') + self.assertIsNone(sig.key_id) + self.assertIsNone(sig.public_key) + self.assertEqual(sig.certificate_path, []) + self.assertEqual(sig.excludes, []) + + def test_full(self) -> None: + pk = JsfPublicKey(kty=JsfKeyType.EC, crv=JsfEcCurve.P_256, x='abc', y='def') + sig = JsfSimpleSignature( + algorithm=JsfAlgorithm.ES256, + value='sig-value', + key_id='my-key', + public_key=pk, + certificate_path=['cert-pem'], + excludes=['field1', 'field2'], + ) + self.assertEqual(sig.public_key, pk) + self.assertEqual(sig.key_id, 'my-key') + self.assertEqual(sig.certificate_path, ['cert-pem']) + self.assertEqual(sig.excludes, ['field1', 'field2']) + + def test_algorithm_as_enum(self) -> None: + sig = JsfSimpleSignature(algorithm=JsfAlgorithm.RS256, value='v') + self.assertIsInstance(sig.algorithm, JsfAlgorithm) + + def test_algorithm_as_uri_string(self) -> None: + sig = JsfSimpleSignature(algorithm='https://example.com/algo', value='v') + self.assertIsInstance(sig.algorithm, str) + self.assertNotIsInstance(sig.algorithm, JsfAlgorithm) + + def test_proprietary_algorithm_non_uri_rejected(self) -> None: + with self.assertRaises(InvalidValueException): + JsfSimpleSignature(algorithm='not-a-uri', value='v') + + def test_proprietary_algorithm_urn_accepted(self) -> None: + sig = JsfSimpleSignature(algorithm='urn:example:my-algo', value='v') + self.assertEqual(sig.algorithm, 'urn:example:my-algo') + + def test_algorithm_enum_roundtrip(self) -> None: + sig = JsfSimpleSignature(algorithm=JsfAlgorithm.RS256, value='AABBCC==') + d = sig._as_dict() + self.assertEqual(d['algorithm'], 'RS256') + restored = JsfSimpleSignature._from_dict(d) + self.assertIsInstance(restored.algorithm, JsfAlgorithm) + self.assertEqual(restored.algorithm, JsfAlgorithm.RS256) + + def test_algorithm_serialized_as_value_string(self) -> None: + sig = JsfSimpleSignature(algorithm=JsfAlgorithm.ED25519, value='v') + d = sig._as_dict() + self.assertEqual(d['algorithm'], 'Ed25519') + + def test_equality(self) -> None: + sig1 = JsfSimpleSignature(algorithm=JsfAlgorithm.ES256, value='v') + sig2 = JsfSimpleSignature(algorithm=JsfAlgorithm.ES256, value='v') + sig3 = JsfSimpleSignature(algorithm=JsfAlgorithm.RS256, value='v') + self.assertEqual(sig1, sig2) + self.assertNotEqual(sig1, sig3) + + def test_not_equal_to_other_modes(self) -> None: + simple = JsfSimpleSignature(algorithm=JsfAlgorithm.ES256, value='v') + multi = JsfSignatureSigners(signers=[JsfSimpleSignature(algorithm=JsfAlgorithm.ES256, value='v')]) + self.assertNotEqual(simple, multi) + + def test_hash_stable(self) -> None: + sig1 = JsfSimpleSignature(algorithm=JsfAlgorithm.ES256, value='v') + sig2 = JsfSimpleSignature(algorithm=JsfAlgorithm.ES256, value='v') + self.assertEqual(hash(sig1), hash(sig2)) + + def test_sorting(self) -> None: + sigs = [ + JsfSimpleSignature(algorithm=JsfAlgorithm.RS256, value='b'), + JsfSimpleSignature(algorithm=JsfAlgorithm.ES256, value='a'), + ] + sorted_sigs = sorted(sigs) + self.assertEqual(len(sorted_sigs), 2) + + def test_repr(self) -> None: + sig = JsfSimpleSignature(algorithm=JsfAlgorithm.ES256, value='v') + self.assertIn('JsfSimpleSignature', repr(sig)) + self.assertIn('ES256', repr(sig)) + + +class TestJsfSignatureSigners(TestCase): + """Tests for JsfSignatureSigners (multisignature mode).""" + + def test_minimal(self) -> None: + signer = JsfSimpleSignature(algorithm=JsfAlgorithm.RS256, value='v1') + sig = JsfSignatureSigners(signers=[signer]) + self.assertEqual(len(sig.signers), 1) + self.assertEqual(sig.signers[0], signer) + + def test_multiple_signers(self) -> None: + s1 = JsfSimpleSignature(algorithm=JsfAlgorithm.RS256, value='v1') + s2 = JsfSimpleSignature(algorithm=JsfAlgorithm.ES256, value='v2') + sig = JsfSignatureSigners(signers=[s1, s2]) + self.assertEqual(len(sig.signers), 2) + + def test_is_jsfsignature_subclass(self) -> None: + sig = JsfSignatureSigners(signers=[JsfSimpleSignature(algorithm=JsfAlgorithm.RS256, value='v')]) + self.assertIsInstance(sig, JsfSignature) + + def test_equality(self) -> None: + s = JsfSimpleSignature(algorithm=JsfAlgorithm.RS256, value='v') + sig1 = JsfSignatureSigners(signers=[s]) + sig2 = JsfSignatureSigners(signers=[s]) + sig3 = JsfSignatureSigners(signers=[JsfSimpleSignature(algorithm=JsfAlgorithm.ES256, value='x')]) + self.assertEqual(sig1, sig2) + self.assertNotEqual(sig1, sig3) + + def test_not_equal_to_chain(self) -> None: + s = JsfSimpleSignature(algorithm=JsfAlgorithm.RS256, value='v') + multi = JsfSignatureSigners(signers=[s]) + chain = JsfSignatureChain(chain=[s]) + self.assertNotEqual(multi, chain) + + def test_hash_stable(self) -> None: + s = JsfSimpleSignature(algorithm=JsfAlgorithm.RS256, value='v') + sig1 = JsfSignatureSigners(signers=[s]) + sig2 = JsfSignatureSigners(signers=[s]) + self.assertEqual(hash(sig1), hash(sig2)) + + def test_repr(self) -> None: + s = JsfSimpleSignature(algorithm=JsfAlgorithm.RS256, value='v') + sig = JsfSignatureSigners(signers=[s]) + self.assertIn('JsfSignatureSigners', repr(sig)) + self.assertIn('1', repr(sig)) + + def test_empty_signers_rejected(self) -> None: + with self.assertRaises(InvalidValueException): + JsfSignatureSigners(signers=[]) + + +class TestJsfSignatureChain(TestCase): + """Tests for JsfSignatureChain (signaturechain mode).""" + + def test_minimal(self) -> None: + signer = JsfSimpleSignature(algorithm=JsfAlgorithm.ED25519, value='xyzSig==') + sig = JsfSignatureChain(chain=[signer]) + self.assertEqual(len(sig.chain), 1) + self.assertEqual(sig.chain[0], signer) + + def test_multiple_in_chain(self) -> None: + s1 = JsfSimpleSignature(algorithm=JsfAlgorithm.RS256, value='v1') + s2 = JsfSimpleSignature(algorithm=JsfAlgorithm.ES256, value='v2') + sig = JsfSignatureChain(chain=[s1, s2]) + self.assertEqual(len(sig.chain), 2) + + def test_is_jsfsignature_subclass(self) -> None: + sig = JsfSignatureChain(chain=[JsfSimpleSignature(algorithm=JsfAlgorithm.RS256, value='v')]) + self.assertIsInstance(sig, JsfSignature) + + def test_equality(self) -> None: + s = JsfSimpleSignature(algorithm=JsfAlgorithm.RS256, value='v') + sig1 = JsfSignatureChain(chain=[s]) + sig2 = JsfSignatureChain(chain=[s]) + sig3 = JsfSignatureChain(chain=[JsfSimpleSignature(algorithm=JsfAlgorithm.ES256, value='x')]) + self.assertEqual(sig1, sig2) + self.assertNotEqual(sig1, sig3) + + def test_not_equal_to_multi(self) -> None: + s = JsfSimpleSignature(algorithm=JsfAlgorithm.RS256, value='v') + chain = JsfSignatureChain(chain=[s]) + multi = JsfSignatureSigners(signers=[s]) + self.assertNotEqual(chain, multi) + + def test_hash_stable(self) -> None: + s = JsfSimpleSignature(algorithm=JsfAlgorithm.RS256, value='v') + sig1 = JsfSignatureChain(chain=[s]) + sig2 = JsfSignatureChain(chain=[s]) + self.assertEqual(hash(sig1), hash(sig2)) + + def test_repr(self) -> None: + s = JsfSimpleSignature(algorithm=JsfAlgorithm.RS256, value='v') + sig = JsfSignatureChain(chain=[s]) + self.assertIn('JsfSignatureChain', repr(sig)) + self.assertIn('1', repr(sig)) + + def test_empty_chain_rejected(self) -> None: + with self.assertRaises(InvalidValueException): + JsfSignatureChain(chain=[]) + + +class TestJsfSignatureBaseClass(TestCase): + """Tests that JsfSignature acts as a proper abstract base class / type for isinstance checks.""" + + def test_simple_is_jsfsignature(self) -> None: + self.assertIsInstance( + JsfSimpleSignature(algorithm=JsfAlgorithm.ES256, value='v'), + JsfSignature, + ) + + def test_signers_is_jsfsignature(self) -> None: + self.assertIsInstance( + JsfSignatureSigners(signers=[JsfSimpleSignature(algorithm=JsfAlgorithm.RS256, value='v')]), + JsfSignature, + ) + + def test_chain_is_jsfsignature(self) -> None: + self.assertIsInstance( + JsfSignatureChain(chain=[JsfSimpleSignature(algorithm=JsfAlgorithm.RS256, value='v')]), + JsfSignature, + ) + + def test_modes_are_distinct_types(self) -> None: + simple = JsfSimpleSignature(algorithm=JsfAlgorithm.ES256, value='v') + multi = JsfSignatureSigners(signers=[JsfSimpleSignature(algorithm=JsfAlgorithm.RS256, value='v')]) + chain = JsfSignatureChain(chain=[JsfSimpleSignature(algorithm=JsfAlgorithm.RS256, value='v')]) + # All are JsfSignature instances + self.assertIsInstance(simple, JsfSignature) + self.assertIsInstance(multi, JsfSignature) + self.assertIsInstance(chain, JsfSignature) + # But they are different types + self.assertEqual(type(simple).__name__, 'JsfSimpleSignature') + self.assertEqual(type(multi).__name__, 'JsfSignatureSigners') + self.assertEqual(type(chain).__name__, 'JsfSignatureChain') + + +class TestJsfAlgorithm(TestCase): + + def test_enum_values(self) -> None: + self.assertEqual(JsfAlgorithm.RS256.value, 'RS256') + self.assertEqual(JsfAlgorithm.ES256.value, 'ES256') + self.assertEqual(JsfAlgorithm.ED25519.value, 'Ed25519') + self.assertEqual(JsfAlgorithm.ED448.value, 'Ed448') + self.assertEqual(JsfAlgorithm.HS512.value, 'HS512') + + def test_all_algorithms_count(self) -> None: + self.assertEqual(len(JsfAlgorithm), 14) + + +class TestJsfKeyType(TestCase): + + def test_enum_values(self) -> None: + self.assertEqual(JsfKeyType.EC.value, 'EC') + self.assertEqual(JsfKeyType.OKP.value, 'OKP') + self.assertEqual(JsfKeyType.RSA.value, 'RSA') + + def test_all_key_types_count(self) -> None: + self.assertEqual(len(JsfKeyType), 3) + + +class TestJsfEcCurve(TestCase): + + def test_enum_values(self) -> None: + self.assertEqual(JsfEcCurve.P_256.value, 'P-256') + self.assertEqual(JsfEcCurve.P_384.value, 'P-384') + self.assertEqual(JsfEcCurve.P_521.value, 'P-521') + + def test_all_curves_count(self) -> None: + self.assertEqual(len(JsfEcCurve), 3) + + +class TestJsfOkpCurve(TestCase): + + def test_enum_values(self) -> None: + self.assertEqual(JsfOkpCurve.ED25519.value, 'Ed25519') + self.assertEqual(JsfOkpCurve.ED448.value, 'Ed448') + + def test_all_curves_count(self) -> None: + self.assertEqual(len(JsfOkpCurve), 2) + + +class TestJsfSignatureXmlBehavior(TestCase): + """Verify that JSF signatures silently produce no XML output.""" + + def test_xml_normalize_returns_none(self) -> None: + sig = JsfSimpleSignature(algorithm=JsfAlgorithm.RS256, value='AABBCC==') + result = _JsfSignatureSerializationHelper.xml_normalize( + sig, element_name='signature', view=None, xmlns=None + ) + self.assertIsNone(result) + + def test_xml_denormalize_returns_none(self) -> None: + from xml.etree.ElementTree import Element # nosec B405 + result = _JsfSignatureSerializationHelper.xml_denormalize( + Element('signature'), default_ns=None + ) + self.assertIsNone(result) + + def test_xml_normalize_signers_returns_none(self) -> None: + sig = JsfSignatureSigners(signers=[ + JsfSimpleSignature(algorithm=JsfAlgorithm.RS256, value='AABBCC==') + ]) + result = _JsfSignatureSerializationHelper.xml_normalize( + sig, element_name='signature', view=None, xmlns=None + ) + self.assertIsNone(result) + + def test_xml_normalize_chain_returns_none(self) -> None: + sig = JsfSignatureChain(chain=[ + JsfSimpleSignature(algorithm=JsfAlgorithm.ED25519, value='xyzSig==') + ]) + result = _JsfSignatureSerializationHelper.xml_normalize( + sig, element_name='signature', view=None, xmlns=None + ) + self.assertIsNone(result)