46 lines
1.4 KiB
Python
46 lines
1.4 KiB
Python
|
|
"""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
|