"""Identifier and signature self-consistency tests. Covers: deterministic key generation, op_id recomputation over canonical bytes, full envelope verification, deterministic re-signing, and rejection of tampered body / sig / op_id. """ from __future__ import annotations import copy import pytest from fpcf import ids, signing, verify from fpcf.errors import ConformanceError from helpers import ( assert_all, tamper_body, tamper_op_id, tamper_sig, valid_single_ops, ) SEED_A = bytes.fromhex("ab" * 32) SEED_B = bytes.fromhex("cd" * 32) # --------------------------------------------------------------------------- # Key generation # --------------------------------------------------------------------------- def test_keypair_generation_is_deterministic_for_a_seed(): kp1 = signing.generate_keypair(seed=SEED_A) kp2 = signing.generate_keypair(seed=SEED_A) assert kp1.key_id == kp2.key_id def test_different_seeds_produce_different_keys(): kp1 = signing.generate_keypair(seed=SEED_A) kp2 = signing.generate_keypair(seed=SEED_B) assert kp1.key_id != kp2.key_id def test_unseeded_keypairs_are_distinct(): kp1 = signing.generate_keypair() kp2 = signing.generate_keypair() assert kp1.key_id != kp2.key_id # --------------------------------------------------------------------------- # op_id self-consistency on the published vectors # --------------------------------------------------------------------------- def test_op_ids_in_valid_vectors_recompute(vectors_dir, manifest): ops = assert_all( valid_single_ops(vectors_dir, manifest), "vector suite must contain valid single-operation vectors", ) for entry, op in ops: assert ids.compute_op_id(op) == op["op_id"], entry["name"] def test_valid_vectors_verify(vectors_dir, manifest): for entry, op in valid_single_ops(vectors_dir, manifest): verify.verify_operation(op) # must not raise # --------------------------------------------------------------------------- # Tamper detection # --------------------------------------------------------------------------- def test_tampered_body_is_rejected(vectors_dir, manifest): _, op = valid_single_ops(vectors_dir, manifest)[0] with pytest.raises(ConformanceError): verify.verify_operation(tamper_body(op)) def test_tampered_signature_is_rejected(vectors_dir, manifest): _, op = valid_single_ops(vectors_dir, manifest)[0] with pytest.raises(ConformanceError): verify.verify_operation(tamper_sig(op)) def test_tampered_op_id_is_rejected(vectors_dir, manifest): _, op = valid_single_ops(vectors_dir, manifest)[0] with pytest.raises(ConformanceError): verify.verify_operation(tamper_op_id(op)) def test_tamper_errors_carry_codes(vectors_dir, manifest): _, op = valid_single_ops(vectors_dir, manifest)[0] for bad in (tamper_body(op), tamper_sig(op), tamper_op_id(op)): try: verify.verify_operation(bad) except ConformanceError as exc: assert isinstance(exc.code, str) and exc.code.startswith("ERR_") else: # pragma: no cover - defensive pytest.fail("expected ConformanceError") # --------------------------------------------------------------------------- # Re-signing # --------------------------------------------------------------------------- def test_resigned_envelope_verifies(vectors_dir, manifest): """Strip sig/op_id from a valid envelope, re-sign with a fresh key. ``sign_envelope`` is responsible for binding the envelope's author/key fields to the supplied keypair, then computing sig and op_id. """ _, op = valid_single_ops(vectors_dir, manifest)[0] env = copy.deepcopy(op) env.pop("sig") env.pop("op_id") kp = signing.generate_keypair(seed=SEED_A) signed = signing.sign_envelope(env, kp) verify.verify_operation(signed) # A different signing key necessarily yields a different operation id. assert signed["op_id"] != op["op_id"] def test_signing_is_deterministic(vectors_dir, manifest): """Ed25519 is deterministic: same envelope + same key => same bytes.""" _, op = valid_single_ops(vectors_dir, manifest)[0] env = copy.deepcopy(op) env.pop("sig") env.pop("op_id") kp = signing.generate_keypair(seed=SEED_A) s1 = signing.sign_envelope(copy.deepcopy(env), kp) s2 = signing.sign_envelope(copy.deepcopy(env), kp) assert s1 == s2