Skip to content

Commit e7a7e61

Browse files
Pigbibicodex
andcommitted
fix: harden identity trust and inventory
Co-Authored-By: Codex <noreply@openai.com>
1 parent f33c08a commit e7a7e61

2 files changed

Lines changed: 173 additions & 42 deletions

File tree

src/quant_advisor_research/identity_lifecycle.py

Lines changed: 78 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
import hashlib
44
import re
5-
from dataclasses import dataclass, field
5+
import weakref
6+
from dataclasses import dataclass
67
from typing import Any
78

89
from .contracts import AdvisoryValidationError, validate_advisory_report
@@ -32,12 +33,6 @@ def __init__(self, code: str) -> None:
3233
super().__init__(code)
3334

3435

35-
@dataclass(frozen=True, slots=True)
36-
class _EvidenceSeal:
37-
kind: str
38-
payload: tuple[object, ...]
39-
40-
4136
@dataclass(frozen=True, slots=True)
4237
class V1ProvisionalBinding:
4338
period_key: str
@@ -48,7 +43,7 @@ class V1ProvisionalBinding:
4843
status: str = "PROVISIONAL"
4944

5045

51-
@dataclass(frozen=True, slots=True)
46+
@dataclass(frozen=True, slots=True, weakref_slot=True)
5247
class VerifiedReportEvidence:
5348
period_key: str
5449
as_of: str
@@ -57,7 +52,6 @@ class VerifiedReportEvidence:
5752
fingerprint_version: str
5853
fingerprint_digest: str
5954
status: str = "VERIFIED_REPORT_EVIDENCE"
60-
_seal: _EvidenceSeal | None = field(default=None, repr=False, compare=False)
6155

6256

6357
@dataclass(frozen=True, slots=True)
@@ -90,11 +84,10 @@ class V2IdentityIndex:
9084
bindings: tuple[V2IdentityBinding, ...]
9185

9286

93-
@dataclass(frozen=True, slots=True)
87+
@dataclass(frozen=True, slots=True, weakref_slot=True)
9488
class VerifiedIdentityEvidence:
9589
binding: V2IdentityBinding
9690
report_digest: str
97-
_seal: _EvidenceSeal | None = field(default=None, repr=False, compare=False)
9891

9992

10093
@dataclass(frozen=True, slots=True)
@@ -113,6 +106,31 @@ class AllocatedIdentity:
113106
allocation_source: str
114107

115108

109+
@dataclass(frozen=True, slots=True, weakref_slot=True)
110+
class CompleteVerifiedIdentityInventory:
111+
identities: tuple[VerifiedIdentityEvidence, ...]
112+
113+
114+
_REPORT_CAPABILITIES: dict[int, weakref.ReferenceType[VerifiedReportEvidence]] = {}
115+
_IDENTITY_CAPABILITIES: dict[int, weakref.ReferenceType[VerifiedIdentityEvidence]] = {}
116+
_INVENTORY_CAPABILITIES: dict[int, weakref.ReferenceType[CompleteVerifiedIdentityInventory]] = {}
117+
118+
119+
def _register_capability(registry: dict[int, weakref.ReferenceType[Any]], value: Any) -> None:
120+
key = id(value)
121+
122+
def discard(reference: weakref.ReferenceType[Any], *, key: int = key) -> None:
123+
if registry.get(key) is reference:
124+
registry.pop(key, None)
125+
126+
registry[key] = weakref.ref(value, discard)
127+
128+
129+
def _has_capability(registry: dict[int, weakref.ReferenceType[Any]], value: Any) -> bool:
130+
reference = registry.get(id(value))
131+
return reference is not None and reference() is value
132+
133+
116134
def _error(code: str) -> IdentityMetadataError:
117135
return IdentityMetadataError(code)
118136

@@ -306,8 +324,10 @@ def make_verified_report_evidence(
306324
raise _error("invalid_schema_version")
307325
if type(fingerprint_digest) is not str or re.fullmatch(r"[0-9a-f]{64}", fingerprint_digest) is None:
308326
raise _error("invalid_fingerprint_digest")
309-
payload = (period_key, as_of, cadence, schema_version, FINGERPRINT_VERSION, fingerprint_digest)
310-
return VerifiedReportEvidence(*payload, _seal=_EvidenceSeal("report", payload))
327+
return VerifiedReportEvidence(
328+
period_key, as_of, cadence, schema_version, FINGERPRINT_VERSION, fingerprint_digest,
329+
status="UNTRUSTED_REPORT_EVIDENCE_CANDIDATE",
330+
)
311331

312332

313333
def _v1_binding_payload(binding: V1ProvisionalBinding) -> dict[str, object]:
@@ -368,14 +388,6 @@ def _compute_report_evidence(report: object, provisional: V1ProvisionalBinding |
368388
return period_key, as_of, cadence, schema_version, digest
369389

370390

371-
def _valid_report_seal(evidence: VerifiedReportEvidence) -> bool:
372-
payload = (
373-
evidence.period_key, evidence.as_of, evidence.cadence, evidence.schema_version,
374-
evidence.fingerprint_version, evidence.fingerprint_digest,
375-
)
376-
return isinstance(evidence._seal, _EvidenceSeal) and evidence._seal == _EvidenceSeal("report", payload)
377-
378-
379391
def verify_report_evidence(
380392
report: object,
381393
*,
@@ -384,14 +396,15 @@ def verify_report_evidence(
384396
) -> VerifiedReportEvidence:
385397
period_key, as_of, cadence, schema_version, digest = _compute_report_evidence(report, provisional)
386398
if expected is not None:
387-
if not isinstance(expected, VerifiedReportEvidence) or not _valid_report_seal(expected):
399+
if not isinstance(expected, VerifiedReportEvidence):
388400
raise _error("report_evidence_untrusted")
389401
if (period_key, as_of, cadence, schema_version, digest) != (
390402
expected.period_key, expected.as_of, expected.cadence, expected.schema_version, expected.fingerprint_digest
391403
):
392404
raise _error("identity_content_conflict")
393-
payload = (period_key, as_of, cadence, schema_version, FINGERPRINT_VERSION, digest)
394-
return VerifiedReportEvidence(*payload, _seal=_EvidenceSeal("report", payload))
405+
evidence = VerifiedReportEvidence(period_key, as_of, cadence, schema_version, FINGERPRINT_VERSION, digest)
406+
_register_capability(_REPORT_CAPABILITIES, evidence)
407+
return evidence
395408

396409

397410
def verify_existing_identity(report: object, binding: V2IdentityBinding) -> VerifiedIdentityEvidence:
@@ -408,16 +421,40 @@ def verify_existing_identity(report: object, binding: V2IdentityBinding) -> Veri
408421
raise _error("identity_metadata_mismatch")
409422
if digest != validated.fingerprint_digest:
410423
raise _error("identity_content_conflict")
411-
payload = (validated, digest)
412-
return VerifiedIdentityEvidence(validated, digest, _EvidenceSeal("identity", payload))
424+
evidence = VerifiedIdentityEvidence(validated, digest)
425+
_register_capability(_IDENTITY_CAPABILITIES, evidence)
426+
return evidence
413427

414428

415429
def _valid_identity_evidence(evidence: VerifiedIdentityEvidence) -> bool:
416-
if not isinstance(evidence, VerifiedIdentityEvidence):
417-
return False
418-
return isinstance(evidence._seal, _EvidenceSeal) and evidence._seal == _EvidenceSeal(
419-
"identity", (evidence.binding, evidence.report_digest)
420-
)
430+
return isinstance(evidence, VerifiedIdentityEvidence) and _has_capability(_IDENTITY_CAPABILITIES, evidence)
431+
432+
433+
def make_complete_identity_inventory(
434+
index: V2IdentityIndex,
435+
identities: tuple[VerifiedIdentityEvidence, ...] | list[VerifiedIdentityEvidence],
436+
) -> CompleteVerifiedIdentityInventory:
437+
if not isinstance(index, V2IdentityIndex) or index.schema_version != 2:
438+
raise _error("identity_inventory_invalid")
439+
if any(not _valid_identity_evidence(item) for item in identities):
440+
raise _error("identity_evidence_untrusted")
441+
bindings = tuple(index.bindings)
442+
try:
443+
_validate_v2_index(bindings)
444+
except IdentityMetadataError:
445+
raise
446+
evidence = tuple(identities)
447+
if len(evidence) != len(bindings):
448+
raise _error("identity_inventory_incomplete")
449+
expected = sorted(bindings, key=lambda item: (item.period_key, item.json_name, item.html_name))
450+
actual = sorted((item.binding for item in evidence), key=lambda item: (item.period_key, item.json_name, item.html_name))
451+
if actual != expected:
452+
raise _error("identity_inventory_incomplete")
453+
inventory = CompleteVerifiedIdentityInventory(tuple(sorted(evidence, key=lambda item: (
454+
item.binding.period_key, item.binding.json_name, item.binding.html_name
455+
))))
456+
_register_capability(_INVENTORY_CAPABILITIES, inventory)
457+
return inventory
421458

422459

423460
def _new_allocated_identity(evidence: VerifiedReportEvidence, *, canonical: bool, source: str) -> AllocatedIdentity:
@@ -434,16 +471,21 @@ def _new_allocated_identity(evidence: VerifiedReportEvidence, *, canonical: bool
434471
def allocate_identity(
435472
evidence: VerifiedReportEvidence,
436473
*,
437-
existing_identities: tuple[VerifiedIdentityEvidence, ...] | list[VerifiedIdentityEvidence] = (),
474+
inventory: CompleteVerifiedIdentityInventory | None = None,
475+
existing_identities: object = None,
438476
current_period_key: str | None = None,
439477
) -> AllocatedIdentity:
440-
if not isinstance(evidence, VerifiedReportEvidence) or not _valid_report_seal(evidence):
478+
if existing_identities is not None:
479+
raise _error("identity_inventory_required")
480+
if not isinstance(evidence, VerifiedReportEvidence) or not _has_capability(_REPORT_CAPABILITIES, evidence):
441481
raise _error("report_evidence_untrusted")
482+
if not isinstance(inventory, CompleteVerifiedIdentityInventory) or not _has_capability(
483+
_INVENTORY_CAPABILITIES, inventory
484+
):
485+
raise _error("identity_inventory_required")
442486
if current_period_key is not None and not isinstance(current_period_key, str):
443487
raise _error("period_mismatch")
444-
identities = tuple(existing_identities)
445-
if any(not _valid_identity_evidence(item) for item in identities):
446-
raise _error("identity_evidence_untrusted")
488+
identities = inventory.identities
447489
bindings = tuple(item.binding for item in identities)
448490
try:
449491
_validate_v2_index(bindings)

tests/test_identity_lifecycle_l2.py

Lines changed: 95 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
import hashlib
44
import json
5+
import gc
6+
import pickle
7+
import weakref
58
from dataclasses import replace
69
from pathlib import Path
710

@@ -13,7 +16,9 @@
1316
IdentityMetadataError,
1417
V1ProvisionalBinding,
1518
V2IdentityBinding,
19+
V2IdentityIndex,
1620
allocate_identity,
21+
make_complete_identity_inventory,
1722
make_verified_report_evidence,
1823
parse_v1_index,
1924
parse_v2_index,
@@ -153,6 +158,49 @@ def test_forged_or_replaced_evidence_cannot_be_allocated() -> None:
153158
allocate_identity(forged, current_period_key=evidence.period_key)
154159

155160

161+
def test_public_candidate_requires_explicit_report_revalidation_for_trust() -> None:
162+
report = build_report()
163+
candidate = make_verified_report_evidence(
164+
as_of=report["as_of"], cadence=report["cadence"], schema_version=report["schema_version"],
165+
fingerprint_digest=digest_for(report),
166+
)
167+
assert candidate.status == "UNTRUSTED_REPORT_EVIDENCE_CANDIDATE"
168+
inventory = make_complete_identity_inventory(V2IdentityIndex(2, ()), ())
169+
170+
with pytest.raises(IdentityMetadataError, match="report_evidence_untrusted"):
171+
allocate_identity(candidate, inventory=inventory, current_period_key=candidate.period_key)
172+
173+
trusted = verify_report_evidence(report, expected=candidate)
174+
assert allocate_identity(trusted, inventory=inventory, current_period_key=trusted.period_key).canonical_identity
175+
176+
177+
def test_equal_copy_and_pickle_roundtrip_do_not_retain_ephemeral_trust() -> None:
178+
trusted = verify_report_evidence(build_report())
179+
copied = replace(trusted)
180+
unpickled = pickle.loads(pickle.dumps(trusted))
181+
inventory = make_complete_identity_inventory(V2IdentityIndex(2, ()), ())
182+
183+
for candidate in (copied, unpickled):
184+
with pytest.raises(IdentityMetadataError, match="report_evidence_untrusted"):
185+
allocate_identity(candidate, inventory=inventory)
186+
187+
188+
def test_legacy_subset_argument_fails_closed() -> None:
189+
trusted = verify_report_evidence(build_report())
190+
191+
with pytest.raises(IdentityMetadataError, match="identity_inventory_required"):
192+
allocate_identity(trusted, existing_identities=[])
193+
194+
195+
def test_report_capability_registry_does_not_keep_evidence_alive() -> None:
196+
trusted = verify_report_evidence(build_report())
197+
reference = weakref.ref(trusted)
198+
del trusted
199+
gc.collect()
200+
201+
assert reference() is None
202+
203+
156204
def test_existing_identity_requires_report_bytes_and_digest_match() -> None:
157205
first = build_report()
158206
binding = v2_binding(first)
@@ -171,8 +219,9 @@ def test_exact_verified_identity_is_reused_immutably() -> None:
171219
report = build_report()
172220
evidence = verify_report_evidence(report)
173221
existing = verify_existing_identity(report, v2_binding(report))
222+
inventory = make_complete_identity_inventory(V2IdentityIndex(2, (existing.binding,)), (existing,))
174223

175-
allocated = allocate_identity(evidence, existing_identities=[existing])
224+
allocated = allocate_identity(evidence, inventory=inventory)
176225

177226
assert allocated.json_name == existing.binding.json_name
178227
assert allocated.html_name == existing.binding.html_name
@@ -181,8 +230,9 @@ def test_exact_verified_identity_is_reused_immutably() -> None:
181230

182231
def test_current_period_without_existing_canonical_allocates_canonical() -> None:
183232
evidence = verify_report_evidence(build_report())
233+
inventory = make_complete_identity_inventory(V2IdentityIndex(2, ()), ())
184234

185-
allocated = allocate_identity(evidence, current_period_key=evidence.period_key)
235+
allocated = allocate_identity(evidence, inventory=inventory, current_period_key=evidence.period_key)
186236

187237
assert allocated.canonical_identity is True
188238
assert ".variant-" not in allocated.json_name
@@ -193,8 +243,9 @@ def test_current_period_with_existing_different_canonical_allocates_variant() ->
193243
new_report = replace_report_content(old_report)
194244
old_identity = verify_existing_identity(old_report, v2_binding(old_report))
195245
new_evidence = verify_report_evidence(new_report)
246+
inventory = make_complete_identity_inventory(V2IdentityIndex(2, (old_identity.binding,)), (old_identity,))
196247

197-
allocated = allocate_identity(new_evidence, existing_identities=[old_identity], current_period_key=new_evidence.period_key)
248+
allocated = allocate_identity(new_evidence, inventory=inventory, current_period_key=new_evidence.period_key)
198249

199250
assert allocated.canonical_identity is False
200251
assert f".variant-{new_evidence.fingerprint_digest}" in allocated.json_name
@@ -203,8 +254,9 @@ def test_current_period_with_existing_different_canonical_allocates_variant() ->
203254

204255
def test_historical_new_content_always_allocates_variant() -> None:
205256
evidence = verify_report_evidence(build_report())
257+
inventory = make_complete_identity_inventory(V2IdentityIndex(2, ()), ())
206258

207-
allocated = allocate_identity(evidence)
259+
allocated = allocate_identity(evidence, inventory=inventory)
208260

209261
assert allocated.canonical_identity is False
210262
assert f".variant-{evidence.fingerprint_digest}" in allocated.html_name
@@ -224,8 +276,45 @@ def test_allocation_is_input_order_independent() -> None:
224276
)
225277
second_identity = verify_existing_identity(second, second_binding)
226278
evidence = verify_report_evidence(build_report(as_of="2026-06-27"))
279+
inventory = make_complete_identity_inventory(
280+
V2IdentityIndex(2, (first_identity.binding, second_identity.binding)),
281+
[first_identity, second_identity],
282+
)
227283

228-
one = allocate_identity(evidence, existing_identities=[first_identity, second_identity])
229-
two = allocate_identity(evidence, existing_identities=[second_identity, first_identity])
284+
one = allocate_identity(evidence, inventory=inventory)
285+
two = allocate_identity(evidence, inventory=inventory)
230286

231287
assert one == two
288+
289+
290+
def test_complete_inventory_requires_one_to_one_full_index_coverage() -> None:
291+
report = build_report()
292+
existing = verify_existing_identity(report, v2_binding(report))
293+
full_index = V2IdentityIndex(2, (existing.binding,))
294+
295+
with pytest.raises(IdentityMetadataError, match="identity_inventory_incomplete"):
296+
make_complete_identity_inventory(full_index, ())
297+
with pytest.raises(IdentityMetadataError, match="identity_inventory_incomplete"):
298+
make_complete_identity_inventory(V2IdentityIndex(2, ()), (existing,))
299+
300+
301+
def test_complete_inventory_rejects_duplicate_or_filtered_bindings() -> None:
302+
report = build_report()
303+
existing = verify_existing_identity(report, v2_binding(report))
304+
duplicate_index = V2IdentityIndex(2, (existing.binding, existing.binding))
305+
306+
with pytest.raises(IdentityMetadataError, match="identity_content_conflict"):
307+
make_complete_identity_inventory(duplicate_index, (existing, existing))
308+
309+
310+
def test_rehydrated_identity_evidence_requires_binding_revalidation() -> None:
311+
report = build_report()
312+
trusted = verify_existing_identity(report, v2_binding(report))
313+
reconstructed = pickle.loads(pickle.dumps(trusted))
314+
315+
with pytest.raises(IdentityMetadataError, match="identity_evidence_untrusted"):
316+
make_complete_identity_inventory(V2IdentityIndex(2, (trusted.binding,)), (reconstructed,))
317+
318+
revalidated = verify_existing_identity(report, trusted.binding)
319+
inventory = make_complete_identity_inventory(V2IdentityIndex(2, (revalidated.binding,)), (revalidated,))
320+
assert allocate_identity(verify_report_evidence(report), inventory=inventory).json_name == revalidated.binding.json_name

0 commit comments

Comments
 (0)