tests/test_signer.py
| 1 | """Tests for MBOMSigner and MBOMVerifier.""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | import pytest |
| 6 | from quantumshield.identity.agent import AgentIdentity |
| 7 | |
| 8 | from pqc_mbom import MBOM, MBOMSigner, MBOMVerifier |
| 9 | from pqc_mbom.errors import SignatureVerificationError |
| 10 | |
| 11 | |
| 12 | def test_sign_populates_fields(sample_mbom: MBOM, creator_identity: AgentIdentity) -> None: |
| 13 | signer = MBOMSigner(creator_identity) |
| 14 | signer.sign(sample_mbom) |
| 15 | assert sample_mbom.signer_did == creator_identity.did |
| 16 | assert sample_mbom.algorithm |
| 17 | assert sample_mbom.signature |
| 18 | assert sample_mbom.public_key |
| 19 | assert sample_mbom.signed_at |
| 20 | |
| 21 | |
| 22 | def test_verify_success(sample_mbom: MBOM, creator_identity: AgentIdentity) -> None: |
| 23 | MBOMSigner(creator_identity).sign(sample_mbom) |
| 24 | result = MBOMVerifier.verify(sample_mbom) |
| 25 | assert result.valid |
| 26 | assert result.signature_valid |
| 27 | assert result.root_hash_valid |
| 28 | assert result.error is None |
| 29 | assert result.signer_did == creator_identity.did |
| 30 | |
| 31 | |
| 32 | def test_tamper_detection(sample_mbom: MBOM, creator_identity: AgentIdentity) -> None: |
| 33 | MBOMSigner(creator_identity).sign(sample_mbom) |
| 34 | # Tamper: change a component's name AFTER signing; the canonical bytes diverge. |
| 35 | sample_mbom.components[0].name = "Evil-Replacement" |
| 36 | result = MBOMVerifier.verify(sample_mbom) |
| 37 | assert not result.valid |
| 38 | # Either the signature OR the root check fails (both will, in fact). |
| 39 | assert not (result.signature_valid and result.root_hash_valid) |
| 40 | |
| 41 | |
| 42 | def test_root_hash_mismatch_detection(sample_mbom: MBOM, creator_identity: AgentIdentity) -> None: |
| 43 | MBOMSigner(creator_identity).sign(sample_mbom) |
| 44 | # Overwrite stored root without recomputing - signature still matches the |
| 45 | # canonical bytes (which include the bad root), but the recomputed root |
| 46 | # will disagree. |
| 47 | sample_mbom.components_root_hash = "f" * 64 |
| 48 | # re-sign with the wrong root baked in |
| 49 | MBOMSigner(creator_identity).sign(sample_mbom) |
| 50 | # Actually the signer always recomputes, so to force a mismatch we |
| 51 | # mutate after signing without recompute: |
| 52 | sample_mbom.components_root_hash = "0" * 64 |
| 53 | result = MBOMVerifier.verify(sample_mbom) |
| 54 | assert not result.valid |
| 55 | assert not result.root_hash_valid |
| 56 | |
| 57 | |
| 58 | def test_verify_or_raise(sample_mbom: MBOM, creator_identity: AgentIdentity) -> None: |
| 59 | # Unsigned MBOM should raise |
| 60 | with pytest.raises(SignatureVerificationError): |
| 61 | MBOMVerifier.verify_or_raise(sample_mbom) |
| 62 | |
| 63 | MBOMSigner(creator_identity).sign(sample_mbom) |
| 64 | # Signed valid MBOM returns result |
| 65 | result = MBOMVerifier.verify_or_raise(sample_mbom) |
| 66 | assert result.valid |
| 67 | |
| 68 | # Tamper then verify_or_raise raises |
| 69 | sample_mbom.components[0].supplier = "Attacker" |
| 70 | with pytest.raises(SignatureVerificationError): |
| 71 | MBOMVerifier.verify_or_raise(sample_mbom) |
| 72 | |