Skip to content

Commit 692b259

Browse files
Pigbibicodex
andcommitted
feat: define canonical contract identity schema
Co-Authored-By: Codex <noreply@openai.com>
1 parent 6cc545a commit 692b259

2 files changed

Lines changed: 558 additions & 0 deletions

File tree

scripts/contract_identity.py

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
"""Pure canonical contract identity schema and digest helpers."""
2+
3+
from __future__ import annotations
4+
5+
import hashlib
6+
import hmac
7+
import json
8+
import re
9+
import unicodedata
10+
from collections import defaultdict
11+
from dataclasses import dataclass
12+
from typing import Any
13+
14+
SCHEMA = "contract_identity.v2"
15+
CANONICALIZER_VERSION = "operator_tokens.v1"
16+
ALLOWED_CATEGORIES = frozenset(
17+
{"bug", "contract", "logic", "performance", "reliability", "security"}
18+
)
19+
ALLOWED_SEVERITIES = frozenset({"critical", "high", "medium", "low"})
20+
ALLOWED_ANCHOR_KINDS = frozenset(
21+
{"config", "endpoint", "field", "identifier", "schema", "symbol", "type"}
22+
)
23+
24+
MAX_REPO_BYTES = 200
25+
MAX_FILE_BYTES = 512
26+
MAX_ANCHOR_BYTES = 256
27+
MAX_TOKEN_BYTES = 256
28+
MAX_EVIDENCE_BYTES = 256
29+
MAX_ITEMS = 32
30+
MAX_TOKENS_PER_CLAUSE = 128
31+
MAX_CANONICAL_BYTES = 16_384
32+
33+
_TOP_REQUIRED = frozenset(
34+
{
35+
"schema",
36+
"canonicalizer_version",
37+
"scope",
38+
"anchors",
39+
"predicates",
40+
"required_behavior",
41+
"forbidden_behavior",
42+
"ordering_constraints",
43+
"evidence",
44+
}
45+
)
46+
_DIGEST_FIELDS = frozenset({"contract_key", "behavior_digest", "fingerprint_v2"})
47+
_OPERATORS = ("===", "!==", ">=", "<=", "==", "!=", "->", "=>", "::", ">", "<")
48+
_TOKEN_RE = re.compile(
49+
r"<SECRET:[A-Z_]+:\d+>|===|!==|>=|<=|==|!=|->|=>|::|>|<|[\w.$]+|[^\s]",
50+
re.UNICODE,
51+
)
52+
_PLACEHOLDER_RE = re.compile(r"<SECRET:[A-Z_]+:\d+>")
53+
_SECRET_PATTERNS = (
54+
("PRIVATE_KEY", re.compile(r"-----BEGIN [^-]+-----.*?-----END [^-]+-----", re.I | re.S)),
55+
("AWS", re.compile(r"\b(?:AKIA|ASIA|AIDA|AROA)[A-Z0-9]{16}\b")),
56+
("SLACK", re.compile(r"\bxox[baprs]-[A-Za-z0-9-]{10,}\b")),
57+
("GITLAB", re.compile(r"\bglpat-[A-Za-z0-9_-]{10,}\b")),
58+
("JWT", re.compile(r"\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b")),
59+
("DSN", re.compile(r"\b[a-z][a-z0-9+.-]*://[^\s/@:]+:[^\s/@]+@[^\s]+", re.I)),
60+
("BEARER", re.compile(r"(?i)\bbearer\s+\S+")),
61+
("CREDENTIAL", re.compile(
62+
r"(?i)(?<!<)\b(?:token|secret|password|api[ _-]?key|authorization|cookie)\b\s*[:=]\s*\S+"
63+
)),
64+
("API_KEY", re.compile(r"\b(?:gh[pousr]_|sk-)[A-Za-z0-9_-]{20,}\b")),
65+
)
66+
67+
68+
class IdentityValidationError(ValueError):
69+
"""Raised when canonical identity input cannot be safely validated."""
70+
71+
72+
@dataclass(frozen=True)
73+
class Scope:
74+
repo: str
75+
file: str
76+
category: str
77+
78+
def as_dict(self) -> dict[str, str]:
79+
return {"repo": self.repo, "file": self.file, "category": self.category}
80+
81+
82+
@dataclass(frozen=True)
83+
class Anchor:
84+
kind: str
85+
value: str
86+
87+
def as_dict(self) -> dict[str, str]:
88+
return {"kind": self.kind, "value": self.value}
89+
90+
91+
@dataclass(frozen=True)
92+
class Evidence:
93+
head_sha: str
94+
diff_digest: str
95+
file: str
96+
location_or_hunk_digest: str
97+
98+
def as_dict(self) -> dict[str, str]:
99+
return {
100+
"head_sha": self.head_sha,
101+
"diff_digest": self.diff_digest,
102+
"file": self.file,
103+
"location_or_hunk_digest": self.location_or_hunk_digest,
104+
}
105+
106+
107+
@dataclass(frozen=True)
108+
class ContractIdentity:
109+
scope: Scope
110+
anchors: tuple[Anchor, ...]
111+
predicates: tuple[tuple[str, ...], ...]
112+
required_behavior: tuple[tuple[str, ...], ...]
113+
forbidden_behavior: tuple[tuple[str, ...], ...]
114+
ordering_constraints: tuple[tuple[str, ...], ...]
115+
evidence: Evidence
116+
severity: str | None
117+
contract_key: str
118+
behavior_digest: str
119+
fingerprint_v2: str
120+
121+
def as_record(self) -> dict[str, Any]:
122+
record: dict[str, Any] = {
123+
"schema": SCHEMA,
124+
"canonicalizer_version": CANONICALIZER_VERSION,
125+
"scope": self.scope.as_dict(),
126+
"anchors": [anchor.as_dict() for anchor in self.anchors],
127+
"predicates": [list(clause) for clause in self.predicates],
128+
"required_behavior": [list(clause) for clause in self.required_behavior],
129+
"forbidden_behavior": [list(clause) for clause in self.forbidden_behavior],
130+
"ordering_constraints": [list(clause) for clause in self.ordering_constraints],
131+
"evidence": self.evidence.as_dict(),
132+
"contract_key": self.contract_key,
133+
"behavior_digest": self.behavior_digest,
134+
"fingerprint_v2": self.fingerprint_v2,
135+
}
136+
if self.severity is not None:
137+
record["severity"] = self.severity
138+
return record
139+
140+
141+
class _SecretRedactor:
142+
def __init__(self, *, allow_placeholders: bool) -> None:
143+
self.allow_placeholders = allow_placeholders
144+
self.counts: dict[str, int] = defaultdict(int)
145+
146+
def redact(self, value: str, field: str) -> str:
147+
if "<SECRET:" in value and not self.allow_placeholders:
148+
raise IdentityValidationError(f"{field} contains a reserved secret placeholder")
149+
if self.allow_placeholders and any(pattern.search(value) for _, pattern in _SECRET_PATTERNS):
150+
raise IdentityValidationError(f"{field} persists an unredacted secret")
151+
text = value
152+
for secret_type, pattern in _SECRET_PATTERNS:
153+
def replacement(_match: re.Match[str], kind: str = secret_type) -> str:
154+
self.counts[kind] += 1
155+
return f"<SECRET:{kind}:{self.counts[kind]}>"
156+
157+
text = pattern.sub(replacement, text)
158+
malformed = re.search(r"<SECRET:[^>]*>", text)
159+
if malformed and not _PLACEHOLDER_RE.fullmatch(malformed.group(0)):
160+
raise IdentityValidationError(f"{field} contains an invalid secret placeholder")
161+
return text
162+
163+
164+
def _exact_fields(value: Any, required: frozenset[str], optional: frozenset[str], field: str) -> dict[str, Any]:
165+
if not isinstance(value, dict) or set(value) != required | (set(value) & optional):
166+
raise IdentityValidationError(f"{field} has missing or extra fields")
167+
return value
168+
169+
170+
def _text(value: Any, field: str, max_bytes: int, redactor: _SecretRedactor) -> str:
171+
if not isinstance(value, str):
172+
raise IdentityValidationError(f"{field} must be a string")
173+
normalized = unicodedata.normalize("NFC", value)
174+
if not normalized or any(unicodedata.category(char).startswith("C") for char in normalized):
175+
raise IdentityValidationError(f"{field} is empty or contains control characters")
176+
normalized = redactor.redact(normalized, field)
177+
if len(normalized.encode("utf-8")) > max_bytes:
178+
raise IdentityValidationError(f"{field} exceeds {max_bytes} bytes")
179+
return normalized
180+
181+
182+
def _repo(value: Any, field: str, redactor: _SecretRedactor) -> str:
183+
repo = _text(value, field, MAX_REPO_BYTES, redactor)
184+
if not re.fullmatch(r"[A-Za-z0-9._-]+/[A-Za-z0-9._-]+", repo):
185+
raise IdentityValidationError(f"{field} must be owner/name")
186+
return repo
187+
188+
189+
def _relative_path(value: Any, field: str, redactor: _SecretRedactor) -> str:
190+
path = _text(value, field, MAX_FILE_BYTES, redactor)
191+
parts = path.split("/")
192+
if (
193+
"<SECRET:" in path
194+
or path.startswith("/")
195+
or "\\" in path
196+
or any(not part or part in {".", ".."} for part in parts)
197+
):
198+
raise IdentityValidationError(f"{field} must be a repository-relative POSIX path")
199+
return path
200+
201+
202+
def _reference(value: Any, field: str, redactor: _SecretRedactor) -> str:
203+
reference = _text(value, field, MAX_EVIDENCE_BYTES, redactor)
204+
if "<SECRET:" in reference:
205+
raise IdentityValidationError(f"{field} must not contain a secret placeholder")
206+
return reference
207+
208+
209+
def _clauses(
210+
value: Any, field: str, redactor: _SecretRedactor, *, allow_empty: bool
211+
) -> tuple[tuple[str, ...], ...]:
212+
if not isinstance(value, list) or len(value) > MAX_ITEMS or (not value and not allow_empty):
213+
raise IdentityValidationError(f"{field} must be a bounded list")
214+
clauses: list[tuple[str, ...]] = []
215+
for index, clause in enumerate(value):
216+
if not isinstance(clause, list) or not clause:
217+
raise IdentityValidationError(f"{field}[{index}] must be a non-empty token list")
218+
tokens: list[str] = []
219+
for raw_token in clause:
220+
token = _text(raw_token, f"{field}[{index}]", MAX_TOKEN_BYTES, redactor)
221+
tokens.extend(_TOKEN_RE.findall(token))
222+
if not tokens or len(tokens) > MAX_TOKENS_PER_CLAUSE:
223+
raise IdentityValidationError(f"{field}[{index}] has invalid token count")
224+
clauses.append(tuple(tokens))
225+
return tuple(clauses)
226+
227+
228+
def _stable_json(value: Any) -> bytes:
229+
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
230+
231+
232+
def _digest(value: Any) -> str:
233+
return hashlib.sha256(_stable_json(value)).hexdigest()
234+
235+
236+
def build_contract_identity(payload: dict[str, Any], *, _allow_placeholders: bool = False) -> ContractIdentity:
237+
top = _exact_fields(payload, _TOP_REQUIRED, frozenset({"severity"}), "identity")
238+
if top["schema"] != SCHEMA or top["canonicalizer_version"] != CANONICALIZER_VERSION:
239+
raise IdentityValidationError("unsupported identity schema or canonicalizer")
240+
redactor = _SecretRedactor(allow_placeholders=_allow_placeholders)
241+
242+
raw_scope = _exact_fields(top["scope"], frozenset({"repo", "file", "category"}), frozenset(), "scope")
243+
category = str(raw_scope["category"]).lower() if isinstance(raw_scope["category"], str) else ""
244+
if category not in ALLOWED_CATEGORIES:
245+
raise IdentityValidationError("scope.category is not controlled")
246+
scope = Scope(
247+
repo=_repo(raw_scope["repo"], "scope.repo", redactor),
248+
file=_relative_path(raw_scope["file"], "scope.file", redactor),
249+
category=category,
250+
)
251+
252+
raw_anchors = top["anchors"]
253+
if not isinstance(raw_anchors, list) or not raw_anchors or len(raw_anchors) > MAX_ITEMS:
254+
raise IdentityValidationError("anchors must be a non-empty bounded list")
255+
anchors: list[Anchor] = []
256+
for index, raw_anchor in enumerate(raw_anchors):
257+
anchor = _exact_fields(raw_anchor, frozenset({"kind", "value"}), frozenset(), f"anchors[{index}]")
258+
kind = str(anchor["kind"]).lower() if isinstance(anchor["kind"], str) else ""
259+
if kind not in ALLOWED_ANCHOR_KINDS:
260+
raise IdentityValidationError(f"anchors[{index}].kind is not controlled")
261+
anchors.append(Anchor(kind, _text(anchor["value"], f"anchors[{index}].value", MAX_ANCHOR_BYTES, redactor)))
262+
263+
predicates = _clauses(top["predicates"], "predicates", redactor, allow_empty=False)
264+
required = _clauses(
265+
top["required_behavior"], "required_behavior", redactor, allow_empty=False
266+
)
267+
forbidden = _clauses(
268+
top["forbidden_behavior"], "forbidden_behavior", redactor, allow_empty=True
269+
)
270+
ordering = _clauses(
271+
top["ordering_constraints"], "ordering_constraints", redactor, allow_empty=True
272+
)
273+
274+
raw_evidence = _exact_fields(
275+
top["evidence"],
276+
frozenset({"head_sha", "diff_digest", "file", "location_or_hunk_digest"}),
277+
frozenset(),
278+
"evidence",
279+
)
280+
head_sha = _text(raw_evidence["head_sha"], "evidence.head_sha", 64, redactor).lower()
281+
diff_digest = _text(raw_evidence["diff_digest"], "evidence.diff_digest", 64, redactor).lower()
282+
if not re.fullmatch(r"[0-9a-f]{7,64}", head_sha) or not re.fullmatch(r"[0-9a-f]{64}", diff_digest):
283+
raise IdentityValidationError("evidence digests are malformed")
284+
evidence = Evidence(
285+
head_sha=head_sha,
286+
diff_digest=diff_digest,
287+
file=_relative_path(raw_evidence["file"], "evidence.file", redactor),
288+
location_or_hunk_digest=_reference(
289+
raw_evidence["location_or_hunk_digest"],
290+
"evidence.location_or_hunk_digest",
291+
redactor,
292+
),
293+
)
294+
if evidence.file != scope.file:
295+
raise IdentityValidationError("evidence.file must match scope.file")
296+
severity = top.get("severity")
297+
if severity is not None:
298+
severity = str(severity).lower() if isinstance(severity, str) else ""
299+
if severity not in ALLOWED_SEVERITIES:
300+
raise IdentityValidationError("severity is not controlled")
301+
302+
contract_payload = {
303+
"schema": SCHEMA,
304+
"canonicalizer_version": CANONICALIZER_VERSION,
305+
"scope": scope.as_dict(),
306+
"anchors": [anchor.as_dict() for anchor in anchors],
307+
"predicates": [list(clause) for clause in predicates],
308+
}
309+
contract_key = _digest(contract_payload)
310+
behavior_payload = {
311+
"contract_key": contract_key,
312+
"required_behavior": [list(clause) for clause in required],
313+
"forbidden_behavior": [list(clause) for clause in forbidden],
314+
"ordering_constraints": [list(clause) for clause in ordering],
315+
}
316+
behavior_digest = _digest(behavior_payload)
317+
fingerprint_v2 = _digest(
318+
{"contract_key": contract_key, "behavior_digest": behavior_digest}
319+
)
320+
identity = ContractIdentity(
321+
scope, tuple(anchors), predicates, required, forbidden, ordering,
322+
evidence, severity, contract_key, behavior_digest, fingerprint_v2,
323+
)
324+
if len(_stable_json(identity.as_record())) > MAX_CANONICAL_BYTES:
325+
raise IdentityValidationError("canonical identity exceeds total byte limit")
326+
return identity
327+
328+
329+
def verify_persisted_identity(record: dict[str, Any]) -> ContractIdentity:
330+
top = _exact_fields(record, _TOP_REQUIRED | _DIGEST_FIELDS, frozenset({"severity"}), "record")
331+
expected = {field: top[field] for field in _DIGEST_FIELDS}
332+
if any(not isinstance(value, str) or not re.fullmatch(r"[0-9a-f]{64}", value) for value in expected.values()):
333+
raise IdentityValidationError("persisted identity digest is malformed")
334+
payload = {key: value for key, value in top.items() if key not in _DIGEST_FIELDS}
335+
identity = build_contract_identity(payload, _allow_placeholders=True)
336+
if not all(hmac.compare_digest(expected[field], getattr(identity, field)) for field in _DIGEST_FIELDS):
337+
raise IdentityValidationError("persisted identity digest mismatch")
338+
return identity
339+
340+
341+
def canonical_json(identity: ContractIdentity) -> str:
342+
return _stable_json(identity.as_record()).decode("utf-8")
343+
344+
345+
def operators() -> tuple[str, ...]:
346+
return _OPERATORS

0 commit comments

Comments
 (0)