Skip to content

feat: Implement JSF (JSON Signature Format) signature support - #994

Open
wiebe-vandendriessche wants to merge 9 commits into
CycloneDX:mainfrom
wiebe-vandendriessche:feat/sig
Open

wiebe-vandendriessche wants to merge 9 commits into
CycloneDX:mainfrom
wiebe-vandendriessche:feat/sig

Conversation

@wiebe-vandendriessche

@wiebe-vandendriessche wiebe-vandendriessche commented Jun 9, 2026 •

Copy link
Copy Markdown

Description

Implements comprehensive JSF (JSON Signature Format) signature support for CycloneDX Python library.

Summary of Changes:

  • Created JsfAlgorithm enum with 14 JWA/RFC8037 algorithms (RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512, ED25519, ED448, HS256, HS384, HS512)
  • Created JsfKeyType enum for key types (EC, OKP, RSA)
  • Implemented JsfPublicKey class with conditional RFC-compliant validation per key type
  • Designed abstract JsfSignature base class with three concrete implementations:
    • JsfSimpleSignature: Simple signature mode (algorithm + value + optional fields)
    • JsfSignatureSigners: Multisignature mode (array of simple signatures under signers)
    • JsfSignatureChain: Chain mode (array of simple signatures under chain)
  • Implemented _JsfSignatureSerializationHelper for JSON serialization/deserialization (ignoring xml serialization/deserialization)
  • Added Optional[JsfSignature] property to Bom, Component, and Service model classes (compositions, annotation, declarations also support sigantures but are not yet implemented in the Python library)
  • Created unit tests covering all classes
  • Updated models.py for snapshots of boms with signatures

Difficulties encountered:

  • Type-safe design prevents invalid nesting at compile time (signers/chain lists only accept JsfSimpleSignature)
  • Three mutually exclusive signature modes properly modeled per JSF specification
  • JSON-only serialization (XML methods return None through a helper _JsfSignatureSerializationHelper)
  • Proper public key validation (EC: crv/x/y, OKP: crv/x, RSA: n/e)

Resolves or fixes issue:

AI Tool Disclosure

  • My contribution includes AI-generated content, as disclosed below:
    • AI Tools: GitHub Copilot
    • LLMs and versions: Claude: Haiku 4.5, Sonnet 4.6
    • Prompts: Architecture design for type-safe JSF signature implementation; test case generation for all signature modes; import optimization and linting fixes

Affirmation

Signed-off-by: Wiebe Vandendriessche <wiebe.vandendriessche@gmail.com>
@wiebe-vandendriessche
wiebe-vandendriessche requested a review from a team as a code owner June 9, 2026 12:47
@codacy-production

codacy-production Bot commented Jun 9, 2026 •

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 95 complexity · 5 duplication

Metric Results
Complexity 95
Duplication 5

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

wiebe-vandendriessche and others added 2 commits June 12, 2026 10:45
Signed-off-by: Wiebe Vandendriessche <146532897+wiebe-vandendriessche@users.noreply.github.com>
Comment thread tests/test_deserialize_xml.py Resolved
Comment thread cyclonedx/model/signature.py Resolved
wiebe-vandendriessche and others added 2 commits June 17, 2026 17:30
Signed-off-by: Wiebe Vandendriessche <wiebe.vandendriessche@gmail.com>
@wiebe-vandendriessche
wiebe-vandendriessche marked this pull request as draft June 26, 2026 14:21
wiebe-vandendriessche and others added 2 commits July 13, 2026 15:44
Signed-off-by: Wiebe Vandendriessche <wiebe.vandendriessche@gmail.com>
Signed-off-by: Wiebe Vandendriessche <146532897+wiebe-vandendriessche@users.noreply.github.com>
@wiebe-vandendriessche
wiebe-vandendriessche marked this pull request as ready for review September 3, 2026 08:28
@wiebe-vandendriessche

Copy link
Copy Markdown
Author

@jkowalleck this should be ready

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new JSF signature models allow construction/deserialization of invalid JSF structures (ambiguous modes, invalid nesting, and post-init invalid mutation) and one unit test currently encodes an invalid algorithm assignment.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Implements JSF (JSON Signature Format) signature support in the CycloneDX Python models, enabling signatures to be represented and serialized in JSON for CycloneDX schema versions >= 1.4.

Changes:

  • Added cyclonedx.model.signature module with JSF enums, public key model, three signature modes, and a JSON-only serialization helper.
  • Extended Bom, Component, and Service models with an optional signature field (JSON-only; omitted in XML).
  • Added unit tests and updated/added snapshot fixtures to cover signatures and enum/schema alignment.
File summaries
File Description
tests/test_model_signature.py New unit tests for JSF signature/key models and polymorphic behavior.
tests/test_enums.py Adds enum-vs-schema validation and JSON rendering cases exercising signatures.
tests/test_deserialize_xml.py Adds XML deserialization-only test for BOMs containing JSON-only fields (signatures).
tests/_data/models.py Adds a snapshot BOM generator including signatures and excludes it from XML roundtrip deep-compare tests.
cyclonedx/model/signature.py Introduces JSF signature domain model + JSON-only (de)serialization helper.
cyclonedx/model/service.py Adds signature: Optional[JsfSignature] to Service with JSON-only serialization mapping.
cyclonedx/model/component.py Adds signature: Optional[JsfSignature] to Component with JSON-only serialization mapping.
cyclonedx/model/bom.py Adds signature: Optional[JsfSignature] to Bom with JSON-only serialization mapping.
tests/_data/snapshots/get_bom_with_signatures-1.7.xml.bin XML snapshot for BOM containing signatures (signatures dropped in XML).
tests/_data/snapshots/get_bom_with_signatures-1.7.json.bin JSON snapshot for BOM containing signatures (signatures present).
tests/_data/snapshots/get_bom_with_signatures-1.6.xml.bin XML snapshot for BOM containing signatures (signatures dropped in XML).
tests/_data/snapshots/get_bom_with_signatures-1.6.json.bin JSON snapshot for BOM containing signatures (signatures present).
tests/_data/snapshots/get_bom_with_signatures-1.5.xml.bin XML snapshot for BOM containing signatures (signatures dropped in XML).
tests/_data/snapshots/get_bom_with_signatures-1.5.json.bin JSON snapshot for BOM containing signatures (signatures present).
tests/_data/snapshots/get_bom_with_signatures-1.4.xml.bin XML snapshot for BOM containing signatures (signatures dropped in XML).
tests/_data/snapshots/get_bom_with_signatures-1.4.json.bin JSON snapshot for BOM containing signatures (signatures present).
tests/_data/snapshots/get_bom_with_signatures-1.3.xml.bin XML snapshot for older schema BOM (no signatures).
tests/_data/snapshots/get_bom_with_signatures-1.3.json.bin JSON snapshot for older schema BOM (no signatures).
tests/_data/snapshots/get_bom_with_signatures-1.2.xml.bin XML snapshot for older schema BOM (no signatures).
tests/_data/snapshots/get_bom_with_signatures-1.2.json.bin JSON snapshot for older schema BOM (no signatures).
tests/_data/snapshots/get_bom_with_signatures-1.1.xml.bin XML snapshot for schema 1.1 (no JSON signatures).
tests/_data/snapshots/get_bom_with_signatures-1.0.xml.bin XML snapshot for schema 1.0 (no JSON signatures).
tests/_data/snapshots/enum_JsfOkpCurve-1.7.json.bin JSON snapshot covering OKP curve enum rendering for schema 1.7.
tests/_data/snapshots/enum_JsfOkpCurve-1.6.json.bin JSON snapshot covering OKP curve enum rendering for schema 1.6.
tests/_data/snapshots/enum_JsfOkpCurve-1.5.json.bin JSON snapshot covering OKP curve enum rendering for schema 1.5.
tests/_data/snapshots/enum_JsfOkpCurve-1.4.json.bin JSON snapshot covering OKP curve enum rendering for schema 1.4.
tests/_data/snapshots/enum_JsfKeyType-1.7.json.bin JSON snapshot covering key type enum rendering for schema 1.7.
tests/_data/snapshots/enum_JsfKeyType-1.6.json.bin JSON snapshot covering key type enum rendering for schema 1.6.
tests/_data/snapshots/enum_JsfKeyType-1.5.json.bin JSON snapshot covering key type enum rendering for schema 1.5.
tests/_data/snapshots/enum_JsfKeyType-1.4.json.bin JSON snapshot covering key type enum rendering for schema 1.4.
tests/_data/snapshots/enum_JsfEcCurve-1.7.json.bin JSON snapshot covering EC curve enum rendering for schema 1.7.
tests/_data/snapshots/enum_JsfEcCurve-1.6.json.bin JSON snapshot covering EC curve enum rendering for schema 1.6.
tests/_data/snapshots/enum_JsfEcCurve-1.5.json.bin JSON snapshot covering EC curve enum rendering for schema 1.5.
tests/_data/snapshots/enum_JsfEcCurve-1.4.json.bin JSON snapshot covering EC curve enum rendering for schema 1.4.
tests/_data/snapshots/enum_JsfAlgorithm-1.7.json.bin JSON snapshot covering algorithm enum rendering for schema 1.7.
tests/_data/snapshots/enum_JsfAlgorithm-1.6.json.bin JSON snapshot covering algorithm enum rendering for schema 1.6.
tests/_data/snapshots/enum_JsfAlgorithm-1.5.json.bin JSON snapshot covering algorithm enum rendering for schema 1.5.
tests/_data/snapshots/enum_JsfAlgorithm-1.4.json.bin JSON snapshot covering algorithm enum rendering for schema 1.4.
Review details
  • Files reviewed: 38/38 changed files
  • Comments generated: 5
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread cyclonedx/model/signature.py Resolved
Comment thread cyclonedx/model/signature.py Resolved
Comment thread cyclonedx/model/signature.py Resolved
Comment thread cyclonedx/model/signature.py Resolved
Comment thread tests/test_model_signature.py Resolved
@wiebe-vandendriessche

Copy link
Copy Markdown
Author

Not an easy one to implement, I'll take a look at Copilots comments

@jkowalleck

Copy link
Copy Markdown
Member

Not an easy one to implement, I'll take a look at Copilots comments

please take your time.
I did not even have time to manually review your valuable contribution, nor copilots review.
As usual - AI is non-authoritative and may make mistakes - since it scans your changes only, not the entirety of this repo - it does not know about previous design decisions and such ...

Even without looking into the details of the PR or AI review, I'd like to say:
It would be okay if the proposed data models would allow creation of invalid data due to not implementing some business logic.
We have a schema-based validator that should take care of formal invalids, and we would not implement a 3rd party (defacto?) standard, since the library users shall take care of this (garbage-in/garbage-out)

Signed-off-by: Wiebe Vandendriessche <wiebe.vandendriessche@gmail.com>
@wiebe-vandendriessche

Copy link
Copy Markdown
Author

Even without looking into the details of the PR or AI review, I'd like to say: It would be okay if the proposed data models would allow creation of invalid data due to not implementing some business logic. We have a schema-based validator that should take care of formal invalids, and we would not implement a 3rd party (defacto?) standard, since the library users shall take care of this (garbage-in/garbage-out)

Thanks for clarifying. I’ve kept the model relatively lightweight, while keeping stricter validation around JsfPublicKey because its fields are conditional on the key type.

@jkowalleck jkowalleck added enhancement New feature or request schema 1.4 labels Sep 15, 2026

jkowalleck commented Nov 6, 2026 •

Copy link
Copy Markdown
Member

review done by Claude Fable 5.1, prompt: "do a review, comment your findings"


I checked out feat/sig (head 3b6be14), read the full diff, ran the test suite locally (test_model_signature, test_enums, test_output_*, test_deserialize_* — all green), ran flake8/mypy (clean), and probed the API manually. Overall this is a clean, well-structured implementation: the three JSF modes are modelled type-safely, the public-key conditional validation matches jsf-0.82, the enum tests are driven from the JSF schema itself, and serialized output validates against the 1.4–1.7 JSON schemas. Nice work.

Below are the things I think should be addressed before merge, ordered by importance.

🔴 Must fix

1. The JSON deserialization path is effectively untested at BOM level.
get_bom_with_signatures was added to all_get_bom_funct_no_xml_roundtrip and that set is subtracted from all_get_bom_funct_valid_immut (tests/_data/models.py). But all_get_bom_funct_valid_immut is also what tests/test_deserialize_json.py::test_prepared iterates over — so the signature BOM is excluded from the JSON round-trip test too, not just the XML one.

Evidence: pytest --collect-only -k signatures lists no test_deserialize_json case, and a coverage run over the whole suite shows _JsfSignatureSerializationHelper.json_denormalize (lines 429–437) and the body of JsfPublicKey._from_dict (lines 210–217) at 0 % coverage. The only _from_dict unit tests cover the algorithm branch and the "rejects forbidden fields" branch — never a successful publicKey parse, never signers/chain dispatch.

(For what it's worth I ran the round-trip by hand — Bom.from_json(json.loads(output)) deep-equals the original — so the code works; it's just not guarded by CI.)

Suggested fix: only subtract the no-XML set in test_deserialize_xml.py (or build a separate all_get_bom_funct_valid_json_immut), and leave test_deserialize_json.py iterating over everything. Alternatively add explicit unit tests for json_denormalize with simple / signers / chain / both-present inputs and a JsfPublicKey._from_dict happy path for each kty.

2. docs/schema-support.rst not updated.
Lines 38, 40 and 61 still say signature is not supported for bom.components, bom.services and bom.signature. This PR makes those statements wrong; please update the table (and note the JSON-only limitation there).

🟡 Should fix / consider

3. Deserialization errors surface as raw KeyError / ValueError.
JsfPublicKey._from_dict({'crv': 'P-256'}) → KeyError: 'kty'; {'kty': 'oct'} → ValueError; JsfSimpleSignature._from_dict({'algorithm': 'RS256'}) → KeyError: 'value'. Elsewhere the module is careful to raise InvalidValueException for user-facing validation; it would be more consistent (and friendlier) to wrap missing required keys / unknown enum values the same way.

4. JsfPublicKey and JsfSimpleSignature fields are plain attributes without setters.
The rest of the library uses @property + setter with validation. Here JsfPublicKey validates only in __init__, so pk.n = 'bad' on an EC key silently produces a schema-invalid {'kty':'EC', ..., 'n':'bad'}. Likewise JsfSimpleSignature.value accepts any type (value=123 serializes as a JSON number, violating "type": "string"). algorithm does have a validating setter, so the inconsistency is noticeable. Consider either making these read-only / properties with validation, or at least documenting that JsfPublicKey is intended to be immutable.

5. _sort_key lives on the serialization helper but drives model __eq__/__hash__/__lt__.
JsfSignature.__eq__ calls _JsfSignatureSerializationHelper._sort_key(...), so the model depends on the serialization helper (and the isinstance ladder there duplicates the subclass hierarchy). Every other model in the lib implements __comparable_tuple per class. Moving the comparable tuple into each concrete class (abstract on the base) would be more idiomatic and avoid the circular conceptual dependency.

6. Two parallel "forbidden fields" mechanisms.
_FORBIDDEN_PUBLIC_KEY_FIELDS is used by _from_dict, while __init__ uses the separate _check_no_rsa_fields / _check_no_y_field / _check_no_ec_okp_fields helpers. The constructor validation already rejects the same combinations, so the extra pre-check in _from_dict is redundant (it only changes the error message). Pick one.

7. Test fixture is semantically inconsistent.
In get_bom_with_signatures, the second signer uses algorithm=JsfAlgorithm.ES384 with an OKP / Ed25519 public key. That's schema-valid (JSF doesn't cross-check), but ES384 is ECDSA-P-384 and would never be paired with an Ed25519 key. Since snapshots are often used as reference examples, suggest JsfAlgorithm.ED25519 there (and e.g. ES256 + EC/P-256 for the component).

8. test_no_xml_roundtrip only asserts "doesn't raise".
It could be stronger at no extra cost: parse the XML, then compare to get_bom() after nulling the three signature attributes — that would prove XML output is otherwise unaffected by the new properties.

⚪ Nits / documentation

  • certificate_path / excludes are typed Optional[list[str]]; the rest of the library accepts Iterable[str] for list-like inputs. Order must be preserved for certificatePath (it's a sorted chain), so a list is the right storage type — just widen the accepted input type.
  • It would be worth a sentence in the module/class docstring that this is model-only: the library does not sign, verify, or canonicalize (RFC 8785/JCS). In particular, re-serializing a signed BOM through this library (property reordering, normalization, dropped unknown fields) will in general invalidate an existing signature. Users will otherwise expect Bom.from_json(...).output_as_string() to keep the signature valid.
  • JsfSignatureSigners/JsfSignatureChain happily accept an empty list. Schema-valid, but an empty signers: [] is meaningless; a warning or at least a docstring note might help.
  • _sort_key compares 'chain'/'signers' string literals against the algorithm string of a simple signature in the first tuple slot. It works, but a leading discriminator (e.g. (0, ...), (1, ...), (2, ...)) would make cross-type ordering explicit instead of incidental.

Verified

  • ✅ JsfSimpleSignature, JsfSignatureSigners, JsfSignatureChain serialize to schema-valid JSON for 1.4–1.7; signature correctly omitted for ≤ 1.3 and for all XML versions.
  • ✅ Manual JSON round-trip of get_bom_with_signatures deep-equals the source model.
  • ✅ Enum members match jsf-0.82.SNAPSHOT.schema.json (algorithm, kty, EC crv, OKP crv).
  • ✅ flake8 / mypy clean on the new module.
  • ✅ Equality / hashing / ordering work across all three subtypes and with public_key=None.

Happy to re-review once #1 and #2 are addressed.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request schema 1.4

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Add support for Component signature [FEAT] add support for JSF Signatures

3 participants