Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/action-ref-v1-conformance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: action-ref-v1 conformance

on:
push:
pull_request:

permissions:
contents: read

jobs:
conformance:
runs-on: ubuntu-latest
steps:
- name: Checkout SafeAgent
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install SafeAgent and focused test dependency
run: |
python -m pip install --upgrade pip
pip install . pytest

- name: Run action-ref focused tests
run: python -m pytest tests/test_attestation_gate.py -q

- name: Verify attestation-gate action-ref-v1 vectors
uses: Composable-Evidence-Criteria/action-ref-conformance@56e25a8353ba02b76edb072c5bd405b56238bb12
with:
mode: verify-impl
command: python scripts/action_ref_v1_impl.py

- name: Verify Mycelium action-ref-v1 vectors
uses: Composable-Evidence-Criteria/action-ref-conformance@56e25a8353ba02b76edb072c5bd405b56238bb12
with:
mode: verify-impl
command: python scripts/action_ref_v1_mycelium_impl.py
39 changes: 37 additions & 2 deletions safeagent_exec_guard/attestation_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from __future__ import annotations

import hashlib
import re
from base64 import urlsafe_b64decode
from datetime import datetime, timezone
from typing import Any
Expand All @@ -30,6 +31,8 @@
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey

_BLOCKING = ("critical", "high")
_ACTION_REF_FIELDS = {"agent_id", "action_type", "scope", "timestamp"}
_ACTION_REF_TS_RE = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$")


def _parse_ts(value: str | None) -> datetime | None:
Expand All @@ -52,7 +55,26 @@ def sha256hex(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()


def validate_action_ref_preimage(preimage: dict) -> None:
"""Enforce the frozen action-ref-v1 four-field grammar before hashing."""
if not isinstance(preimage, dict):
raise ValueError("action_ref preimage must be an object")
if set(preimage) != _ACTION_REF_FIELDS:
raise ValueError("action_ref preimage must contain exactly agent_id, action_type, scope, timestamp")
for field in ("agent_id", "action_type", "scope"):
if not isinstance(preimage[field], str):
raise ValueError(f"action_ref {field} must be a string")
timestamp = preimage["timestamp"]
if not isinstance(timestamp, str) or not _ACTION_REF_TS_RE.fullmatch(timestamp):
raise ValueError("action_ref timestamp must be YYYY-MM-DDTHH:MM:SS.mmmZ")
try:
datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%fZ")
except ValueError as exc:
raise ValueError("action_ref timestamp is not a valid UTC instant") from exc


def compute_action_ref(preimage: dict) -> str:
validate_action_ref_preimage(preimage)
return sha256hex(jcs(preimage))


Expand Down Expand Up @@ -186,9 +208,22 @@ def gate(
return {"decision": "DENY", "reason": f"safety_findings: {parts}", "entities": entities}

action_ref_preimage = attestation.get("action_ref_preimage")
action_ref = compute_action_ref(action_ref_preimage) if action_ref_preimage else None
if action_ref_preimage:
try:
action_ref = compute_action_ref(action_ref_preimage)
except ValueError:
return {"decision": "DENY", "reason": "action_ref_preimage_invalid"}
else:
action_ref = None
return {"decision": "ALLOW", "reason": "admit", "action_ref": action_ref,
"binding_digest": expected}


__all__ = ["gate", "verify_signature", "compute_action_ref", "compute_binding_digest", "jcs"]
__all__ = [
"gate",
"verify_signature",
"compute_action_ref",
"compute_binding_digest",
"validate_action_ref_preimage",
"jcs",
]
Loading
Loading