approval-engine/approval_engine/binding.py

46 lines
1.4 KiB
Python
Raw Normal View History

"""Canonical binding and native digest.
The native digest is SHA-256 over sorted-key JSON of five fields. It is not
Go json.Marshal of a flex-auth CheckRequest that value, when known at
issue, is stored separately as pdp_digest.
"""
from __future__ import annotations
import hashlib
import json
import re
from typing import Any
DIGEST_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
BINDING_FIELDS = ("action", "actor", "principal", "purpose", "target")
def canonical_binding(binding: dict[str, Any]) -> dict[str, Any]:
missing = [k for k in BINDING_FIELDS if k not in binding or binding[k] in (None, "")]
if missing:
raise ValueError(f"binding missing {missing}")
if not isinstance(binding["target"], dict):
raise ValueError("binding.target must be an object")
return {k: binding[k] for k in BINDING_FIELDS}
def canonical_json(value: Any) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode(
"utf-8"
)
def binding_digest(binding: dict[str, Any]) -> str:
payload = canonical_json(canonical_binding(binding))
return "sha256:" + hashlib.sha256(payload).hexdigest()
def require_digest(value: str | None) -> str | None:
if value is None:
return None
if not DIGEST_RE.match(value):
raise ValueError("digest must match sha256:<64 lowercase hex>")
return value