tests/test_signer.py
3.9 KB · 118 lines · python Raw
1 """Tests for ChunkSigner."""
2
3 from __future__ import annotations
4
5 from dataclasses import replace
6
7 from quantumshield.identity.agent import AgentIdentity
8
9 from pqc_rag_signing import ChunkMetadata, ChunkSigner, SignedChunk
10
11
12 def test_sign_chunk_produces_valid_envelope(
13 signer: ChunkSigner,
14 sample_metadata: ChunkMetadata,
15 ) -> None:
16 chunk = signer.sign_chunk("hello world", sample_metadata)
17 assert chunk.chunk_id.startswith("chunk-")
18 assert chunk.text == "hello world"
19 assert chunk.content_hash
20 assert len(chunk.content_hash) == 64
21 assert chunk.signer_did == signer.identity.did
22 assert chunk.algorithm == signer.identity.signing_keypair.algorithm.value
23 assert chunk.signature
24 assert chunk.public_key
25 assert chunk.signed_at
26 assert chunk.corpus_id == "test-corpus"
27 assert chunk.nonce
28
29
30 def test_sign_chunk_verifies(sample_signed_chunk: SignedChunk) -> None:
31 result = ChunkSigner.verify_chunk(sample_signed_chunk)
32 assert result.valid
33 assert result.chunk_id == sample_signed_chunk.chunk_id
34 assert result.signer_did == sample_signed_chunk.signer_did
35 assert result.error is None
36
37
38 def test_tampered_text_fails_verification(sample_signed_chunk: SignedChunk) -> None:
39 tampered = replace(sample_signed_chunk, text="MALICIOUS TEXT")
40 result = ChunkSigner.verify_chunk(tampered)
41 assert not result.valid
42 assert "content hash mismatch" in (result.error or "")
43
44
45 def test_tampered_metadata_fails_verification(
46 sample_signed_chunk: SignedChunk,
47 ) -> None:
48 new_meta = ChunkMetadata(
49 source="OTHER.txt",
50 chunk_index=99,
51 total_chunks=999,
52 )
53 tampered = replace(sample_signed_chunk, metadata=new_meta)
54 result = ChunkSigner.verify_chunk(tampered)
55 assert not result.valid
56 assert "content hash mismatch" in (result.error or "")
57
58
59 def test_tampered_signature_fails_verification(
60 sample_signed_chunk: SignedChunk,
61 ) -> None:
62 # Flip a byte in the signature (hex)
63 sig = sample_signed_chunk.signature
64 flipped = ("00" if sig[0:2] != "00" else "11") + sig[2:]
65 tampered = replace(sample_signed_chunk, signature=flipped)
66 result = ChunkSigner.verify_chunk(tampered)
67 # With real signatures (ML-DSA / Ed25519) this should be invalid.
68 # With stub backend verify() always returns True, so only check
69 # that the result is well-formed. In both real backends available in
70 # tests (liboqs, ed25519) the signature check fails.
71 if result.valid:
72 # Stub backend: signature check can't detect tampering. Accept.
73 return
74 assert not result.valid
75
76
77 def test_wrong_public_key_fails_verification(
78 sample_signed_chunk: SignedChunk,
79 ) -> None:
80 other = AgentIdentity.create("other-agent")
81 wrong = replace(
82 sample_signed_chunk,
83 public_key=other.signing_keypair.public_key.hex(),
84 )
85 result = ChunkSigner.verify_chunk(wrong)
86 # Real backends (ed25519 / liboqs) will catch this.
87 if result.valid:
88 return
89 assert not result.valid
90
91
92 def test_sign_batch_auto_computes_metadata(signer: ChunkSigner) -> None:
93 texts = ["aaa", "bbbb", "cc"]
94 signed = signer.sign_chunks(texts, source="batch.txt")
95 assert len(signed) == 3
96 for i, c in enumerate(signed):
97 assert c.metadata.chunk_index == i
98 assert c.metadata.total_chunks == 3
99 assert c.metadata.source == "batch.txt"
100 # Offsets are cumulative
101 assert signed[0].metadata.start_offset == 0
102 assert signed[0].metadata.end_offset == 3
103 assert signed[1].metadata.start_offset == 3
104 assert signed[1].metadata.end_offset == 7
105 assert signed[2].metadata.start_offset == 7
106 assert signed[2].metadata.end_offset == 9
107
108
109 def test_signer_nonce_uniqueness(
110 signer: ChunkSigner,
111 sample_metadata: ChunkMetadata,
112 ) -> None:
113 a = signer.sign_chunk("same text", sample_metadata)
114 b = signer.sign_chunk("same text", sample_metadata)
115 assert a.nonce != b.nonce
116 assert a.content_hash != b.content_hash
117 assert a.chunk_id != b.chunk_id
118