-
Notifications
You must be signed in to change notification settings - Fork 0
Add fixed trusted weekly workflow identity foundation #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| # Trusted weekly workflow identity v1 | ||
|
|
||
| This is a pure PR-A0 foundation. It performs no filesystem, GitHub Actions, | ||
| artifact, workflow, producer, or rerun operation. | ||
|
|
||
| ## Fixed identity | ||
|
|
||
| The only supported identity is compiled into the module: | ||
|
|
||
| - repository: `QuantStrategyLab/PoliticalEventTrackingResearch` | ||
| - workflow path: `.github/workflows/pert_weekly_period_lock_harness.yml` | ||
| - workflow ref: `QuantStrategyLab/PoliticalEventTrackingResearch/.github/workflows/pert_weekly_period_lock_harness.yml@refs/heads/main` | ||
|
|
||
| `trusted_workflow_identity()` is the zero-override API. Callers cannot provide | ||
| another repository, path, branch, tag, or ref as a trust root. The reviewed | ||
| workflow SHA is separate runtime evidence: it must be exactly 40 lowercase hex | ||
| characters and is bound to the fixed identity by | ||
| `validate_trusted_workflow_identity()`. | ||
|
|
||
| The workflow path is a planned dedicated trusted harness path. This PR does not | ||
| create or modify that workflow and does not grant Actions permissions. | ||
|
|
||
| ## Wire contract | ||
|
|
||
| The canonical `pert.trusted_workflow_identity.v1` wire object has exactly: | ||
|
|
||
| ```json | ||
| {"identity_version":"pert.trusted_workflow_identity.v1","repository":"QuantStrategyLab/PoliticalEventTrackingResearch","reviewed_workflow_sha":"<40 lowercase hex>","workflow_path":".github/workflows/pert_weekly_period_lock_harness.yml","workflow_ref":"QuantStrategyLab/PoliticalEventTrackingResearch/.github/workflows/pert_weekly_period_lock_harness.yml@refs/heads/main"} | ||
| ``` | ||
|
|
||
| Unknown, missing, duplicate, noncanonical, Unicode/control, alias, wrong | ||
| repository/path/ref, and malformed SHA values fail closed with sanitized | ||
| `TrustedWorkflowIdentityError` codes. The parser and serializer use the same | ||
| fixed identity; they do not accept legacy or future versions. | ||
|
|
||
| ## Next boundary | ||
|
|
||
| The later bundle integration must derive repository/path/ref from this typed | ||
| identity, validate `period_lock.workflow_ref` against it, and parse a manifest | ||
| structurally before comparing reconstructed canonical bytes. A privileged | ||
| workflow and `actions:read` remain outside PR-A0. |
168 changes: 168 additions & 0 deletions
168
src/political_event_tracking_research/trusted_workflow_identity.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,168 @@ | ||
| """Pure, code-reviewed identity for the future trusted weekly harness. | ||
|
|
||
| This module has no workflow, filesystem, GitHub, or artifact access. Runtime | ||
| workflow SHA evidence is validated separately from the fixed repository and | ||
| workflow path/ref; callers cannot supply an alternate trust root. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import json | ||
| import re | ||
| from dataclasses import dataclass | ||
|
|
||
| IDENTITY_VERSION = "pert.trusted_workflow_identity.v1" | ||
| TRUSTED_REPOSITORY = "QuantStrategyLab/PoliticalEventTrackingResearch" | ||
| TRUSTED_WORKFLOW_PATH = ".github/workflows/pert_weekly_period_lock_harness.yml" | ||
| TRUSTED_WORKFLOW_REF = ( | ||
| f"{TRUSTED_REPOSITORY}/{TRUSTED_WORKFLOW_PATH}@refs/heads/main" | ||
| ) | ||
|
|
||
| _SHA_RE = re.compile(r"^[0-9a-f]{40}$") | ||
| _WIRE_KEYS = frozenset( | ||
| { | ||
| "identity_version", | ||
| "repository", | ||
| "workflow_path", | ||
| "workflow_ref", | ||
| "reviewed_workflow_sha", | ||
| } | ||
| ) | ||
|
|
||
|
|
||
| class TrustedWorkflowIdentityError(ValueError): | ||
| """Stable, sanitized identity contract error.""" | ||
|
|
||
| def __init__(self, code: str) -> None: | ||
| self.code = code | ||
| super().__init__(code) | ||
|
|
||
|
|
||
| def _error(code: str) -> TrustedWorkflowIdentityError: | ||
| return TrustedWorkflowIdentityError(code) | ||
|
|
||
|
|
||
| @dataclass(frozen=True, slots=True, init=False) | ||
| class TrustedWorkflowIdentity: | ||
| """Immutable fixed identity; construction has no override parameters.""" | ||
|
|
||
| repository: str | ||
| workflow_path: str | ||
| workflow_ref: str | ||
|
|
||
| def __init__(self) -> None: | ||
| object.__setattr__(self, "repository", TRUSTED_REPOSITORY) | ||
| object.__setattr__(self, "workflow_path", TRUSTED_WORKFLOW_PATH) | ||
| object.__setattr__(self, "workflow_ref", TRUSTED_WORKFLOW_REF) | ||
|
|
||
|
|
||
| def trusted_workflow_identity() -> TrustedWorkflowIdentity: | ||
| """Return the only supported repository/workflow identity.""" | ||
|
|
||
| return TrustedWorkflowIdentity() | ||
|
|
||
|
|
||
| def _reviewed_sha(value: object) -> str: | ||
| if type(value) is not str or not _SHA_RE.fullmatch(value): | ||
| raise _error("reviewed_workflow_sha_invalid") | ||
| return value | ||
|
|
||
|
|
||
| @dataclass(frozen=True, slots=True) | ||
| class TrustedWorkflowEvidence: | ||
| """Fixed identity plus independently supplied reviewed workflow SHA.""" | ||
|
|
||
| identity: TrustedWorkflowIdentity | ||
| reviewed_workflow_sha: str | ||
|
|
||
| def __post_init__(self) -> None: | ||
| if ( | ||
| type(self.identity) is not TrustedWorkflowIdentity | ||
| or self.identity.repository != TRUSTED_REPOSITORY | ||
| or self.identity.workflow_path != TRUSTED_WORKFLOW_PATH | ||
| or self.identity.workflow_ref != TRUSTED_WORKFLOW_REF | ||
| ): | ||
| raise _error("trusted_workflow_identity_mismatch") | ||
| _reviewed_sha(self.reviewed_workflow_sha) | ||
|
|
||
|
|
||
| def validate_trusted_workflow_identity( | ||
| workflow_ref: object, reviewed_workflow_sha: object | ||
| ) -> TrustedWorkflowEvidence: | ||
| """Validate fixed workflow ref and independent runtime SHA evidence.""" | ||
|
|
||
| if type(workflow_ref) is not str or workflow_ref != TRUSTED_WORKFLOW_REF: | ||
| raise _error("trusted_workflow_identity_mismatch") | ||
| return TrustedWorkflowEvidence(TrustedWorkflowIdentity(), _reviewed_sha(reviewed_workflow_sha)) | ||
|
|
||
|
|
||
| def _canonical_bytes(value: dict[str, object]) -> bytes: | ||
| try: | ||
| return json.dumps(value, ensure_ascii=True, sort_keys=True, separators=(",", ":"), allow_nan=False).encode( | ||
| "ascii" | ||
| ) | ||
| except (TypeError, ValueError, UnicodeError, OverflowError, RecursionError): | ||
| raise _error("trusted_workflow_serialization_invalid") from None | ||
|
|
||
|
|
||
| def serialize_trusted_workflow_evidence(evidence: TrustedWorkflowEvidence) -> bytes: | ||
| if type(evidence) is not TrustedWorkflowEvidence: | ||
| raise _error("trusted_workflow_evidence_invalid") | ||
| if ( | ||
| evidence.identity.repository != TRUSTED_REPOSITORY | ||
| or evidence.identity.workflow_path != TRUSTED_WORKFLOW_PATH | ||
| or evidence.identity.workflow_ref != TRUSTED_WORKFLOW_REF | ||
| ): | ||
| raise _error("trusted_workflow_identity_mismatch") | ||
| return _canonical_bytes( | ||
| { | ||
| "identity_version": IDENTITY_VERSION, | ||
| "repository": TRUSTED_REPOSITORY, | ||
| "workflow_path": TRUSTED_WORKFLOW_PATH, | ||
| "workflow_ref": TRUSTED_WORKFLOW_REF, | ||
| "reviewed_workflow_sha": _reviewed_sha(evidence.reviewed_workflow_sha), | ||
| } | ||
| ) | ||
|
|
||
|
|
||
| def _parse_wire_value(value: object) -> TrustedWorkflowEvidence: | ||
| if type(value) is not dict or set(value) != _WIRE_KEYS: | ||
| raise _error("trusted_workflow_shape_invalid") | ||
| if value["identity_version"] != IDENTITY_VERSION: | ||
| raise _error("trusted_workflow_version_invalid") | ||
| if type(value["identity_version"]) is not str: | ||
| raise _error("trusted_workflow_version_invalid") | ||
| if value["repository"] != TRUSTED_REPOSITORY or type(value["repository"]) is not str: | ||
| raise _error("trusted_workflow_identity_mismatch") | ||
| if value["workflow_path"] != TRUSTED_WORKFLOW_PATH or type(value["workflow_path"]) is not str: | ||
| raise _error("trusted_workflow_identity_mismatch") | ||
| if value["workflow_ref"] != TRUSTED_WORKFLOW_REF or type(value["workflow_ref"]) is not str: | ||
| raise _error("trusted_workflow_identity_mismatch") | ||
| return validate_trusted_workflow_identity(value["workflow_ref"], value["reviewed_workflow_sha"]) | ||
|
|
||
|
|
||
| def parse_trusted_workflow_evidence(raw: bytes) -> TrustedWorkflowEvidence: | ||
| if type(raw) is not bytes: | ||
| raise _error("trusted_workflow_wire_invalid") | ||
|
|
||
| def pairs(items: list[tuple[str, object]]) -> dict[str, object]: | ||
| result: dict[str, object] = {} | ||
| for key, item in items: | ||
| if key in result: | ||
| raise _error("trusted_workflow_duplicate_key") | ||
| result[key] = item | ||
| return result | ||
|
|
||
| def reject_constant(_: str) -> None: | ||
| raise _error("trusted_workflow_wire_invalid") | ||
|
|
||
| try: | ||
| value = json.loads(raw.decode("ascii"), object_pairs_hook=pairs, parse_constant=reject_constant) | ||
| except TrustedWorkflowIdentityError: | ||
| raise | ||
| except (UnicodeError, json.JSONDecodeError, TypeError, ValueError, RecursionError): | ||
| raise _error("trusted_workflow_wire_invalid") from None | ||
| evidence = _parse_wire_value(value) | ||
| if serialize_trusted_workflow_evidence(evidence) != raw: | ||
| raise _error("trusted_workflow_noncanonical") | ||
| return evidence | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import json | ||
| from dataclasses import FrozenInstanceError, replace | ||
|
|
||
| import pytest | ||
|
|
||
| import political_event_tracking_research.trusted_workflow_identity as module | ||
| from political_event_tracking_research.trusted_workflow_identity import ( | ||
| IDENTITY_VERSION, | ||
| TRUSTED_REPOSITORY, | ||
| TRUSTED_WORKFLOW_PATH, | ||
| TRUSTED_WORKFLOW_REF, | ||
| TrustedWorkflowIdentityError, | ||
| TrustedWorkflowEvidence, | ||
| parse_trusted_workflow_evidence, | ||
| serialize_trusted_workflow_evidence, | ||
| trusted_workflow_identity, | ||
| validate_trusted_workflow_identity, | ||
| ) | ||
|
|
||
|
|
||
| SHA = "a" * 40 | ||
|
|
||
|
|
||
| def valid_wire() -> dict[str, str]: | ||
| return { | ||
| "identity_version": IDENTITY_VERSION, | ||
| "repository": TRUSTED_REPOSITORY, | ||
| "workflow_path": TRUSTED_WORKFLOW_PATH, | ||
| "workflow_ref": TRUSTED_WORKFLOW_REF, | ||
| "reviewed_workflow_sha": SHA, | ||
| } | ||
|
|
||
|
|
||
| def test_fixed_identity_has_no_runtime_override() -> None: | ||
| identity = trusted_workflow_identity() | ||
| assert identity.repository == TRUSTED_REPOSITORY | ||
| assert identity.workflow_path == TRUSTED_WORKFLOW_PATH | ||
| assert identity.workflow_ref == TRUSTED_WORKFLOW_REF | ||
| with pytest.raises(TypeError): | ||
| module.TrustedWorkflowIdentity(TRUSTED_REPOSITORY, "other.yml", TRUSTED_WORKFLOW_REF) # type: ignore[call-arg] | ||
| with pytest.raises(FrozenInstanceError): | ||
| identity.repository = "QuantStrategyLab/Other" # type: ignore[misc] | ||
| assert trusted_workflow_identity().repository == TRUSTED_REPOSITORY | ||
|
|
||
|
|
||
| def test_evidence_constructor_and_replace_revalidate_invariants() -> None: | ||
| evidence = validate_trusted_workflow_identity(TRUSTED_WORKFLOW_REF, SHA) | ||
| assert replace(evidence, reviewed_workflow_sha="b" * 40).reviewed_workflow_sha == "b" * 40 | ||
| with pytest.raises(TrustedWorkflowIdentityError, match="reviewed_workflow_sha_invalid"): | ||
| replace(evidence, reviewed_workflow_sha="not-a-sha") | ||
|
|
||
| forged_identity = object.__new__(module.TrustedWorkflowIdentity) | ||
| object.__setattr__(forged_identity, "repository", "QuantStrategyLab/Other") | ||
| object.__setattr__(forged_identity, "workflow_path", TRUSTED_WORKFLOW_PATH) | ||
| object.__setattr__(forged_identity, "workflow_ref", TRUSTED_WORKFLOW_REF) | ||
| with pytest.raises(TrustedWorkflowIdentityError, match="trusted_workflow_identity_mismatch"): | ||
| TrustedWorkflowEvidence(forged_identity, SHA) | ||
|
|
||
|
|
||
| def test_runtime_sha_and_fixed_workflow_identity_round_trip() -> None: | ||
| evidence = validate_trusted_workflow_identity(TRUSTED_WORKFLOW_REF, SHA) | ||
| wire = serialize_trusted_workflow_evidence(evidence) | ||
| assert parse_trusted_workflow_evidence(wire) == evidence | ||
| assert wire == serialize_trusted_workflow_evidence(parse_trusted_workflow_evidence(wire)) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "workflow_ref", | ||
| [ | ||
| "QuantStrategyLab/Other/.github/workflows/pert_weekly_period_lock_harness.yml@refs/heads/main", | ||
| "QuantStrategyLab/PoliticalEventTrackingResearch/.github/workflows/other.yml@refs/heads/main", | ||
| TRUSTED_WORKFLOW_REF.replace("refs/heads/main", "refs/tags/main"), | ||
| TRUSTED_WORKFLOW_REF.replace("PoliticalEventTrackingResearch", "politicaleventtrackingresearch"), | ||
| TRUSTED_WORKFLOW_REF.replace(".github/workflows/", ".github//workflows/"), | ||
| TRUSTED_WORKFLOW_REF + "\n", | ||
| SHA, | ||
| ], | ||
| ) | ||
| def test_wrong_workflow_identity_is_rejected(workflow_ref: str) -> None: | ||
| with pytest.raises(TrustedWorkflowIdentityError, match="trusted_workflow_identity_mismatch"): | ||
| validate_trusted_workflow_identity(workflow_ref, SHA) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("sha", ["a" * 39, "A" * 40, "g" * 40, "refs/heads/main", True, 1]) | ||
| def test_reviewed_sha_is_strict_full_hex(sha: object) -> None: | ||
| with pytest.raises(TrustedWorkflowIdentityError, match="reviewed_workflow_sha_invalid"): | ||
| validate_trusted_workflow_identity(TRUSTED_WORKFLOW_REF, sha) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("mutation", ["unknown", "missing", "wrong_repo", "wrong_path", "wrong_ref", "wrong_sha"]) | ||
| def test_wire_is_exact_and_sanitized(mutation: str) -> None: | ||
| wire = valid_wire() | ||
| if mutation == "unknown": | ||
| wire["debug"] = "secret" | ||
| elif mutation == "missing": | ||
| del wire["workflow_path"] | ||
| elif mutation == "wrong_repo": | ||
| wire["repository"] = "QuantStrategyLab/Other" | ||
| elif mutation == "wrong_path": | ||
| wire["workflow_path"] = "other.yml" | ||
| elif mutation == "wrong_ref": | ||
| wire["workflow_ref"] = TRUSTED_WORKFLOW_REF.replace("main", "feature") | ||
| else: | ||
| wire["reviewed_workflow_sha"] = "b" * 40 | ||
| with pytest.raises(TrustedWorkflowIdentityError): | ||
| parse_trusted_workflow_evidence(json.dumps(wire).encode()) | ||
|
|
||
|
|
||
| def test_duplicate_and_noncanonical_wire_fail_closed() -> None: | ||
| wire = json.dumps(valid_wire(), sort_keys=True, separators=(",", ":")) | ||
| duplicate = wire.replace( | ||
| '"workflow_path":"' + TRUSTED_WORKFLOW_PATH + '"', | ||
| '"workflow_path":"' + TRUSTED_WORKFLOW_PATH + '","workflow_path":"' + TRUSTED_WORKFLOW_PATH + '"', | ||
| ) | ||
| with pytest.raises(TrustedWorkflowIdentityError, match="trusted_workflow_duplicate_key"): | ||
| parse_trusted_workflow_evidence(duplicate.encode()) | ||
| with pytest.raises(TrustedWorkflowIdentityError, match="trusted_workflow_noncanonical"): | ||
| parse_trusted_workflow_evidence(json.dumps(valid_wire()).encode()) | ||
|
|
||
|
|
||
| def test_unexpected_runtime_error_is_not_broad_caught(monkeypatch: pytest.MonkeyPatch) -> None: | ||
| def fail(*_: object, **__: object) -> bytes: | ||
| raise RuntimeError("programming failure") | ||
|
|
||
| monkeypatch.setattr(module.json, "dumps", fail) | ||
| with pytest.raises(RuntimeError, match="programming failure"): | ||
| serialize_trusted_workflow_evidence(validate_trusted_workflow_identity(TRUSTED_WORKFLOW_REF, SHA)) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the later integration has a SHA from a miswired or untrusted workflow, it can still call
TrustedWorkflowEvidence(trusted_workflow_identity(), sha)and serialize a canonical trusted wire object without ever comparing the runtimegithub.workflow_ref. Because this public dataclass generates an accepting constructor, it bypasses the only API that validates the workflow ref; keep construction internal or gate it with a private factory token so callers must usevalidate_trusted_workflow_identity().Useful? React with 👍 / 👎.