Harden Sigil: sigil/sessions.py

This commit is contained in:
bigpickle 2026-10-05 21:24:43 +08:00
parent 854db7c6a7
commit 128a8db763

View File

@ -17,6 +17,8 @@ Two properties worth stating out loud:
from __future__ import annotations from __future__ import annotations
import hashlib
import json
import secrets import secrets
import time import time
from typing import List, Optional from typing import List, Optional
@ -40,6 +42,16 @@ AGENT_SCOPES = (AGENT_ACT, "view:read", "view:write", "secret:use")
DEFAULT_AGENT_TTL = 900 # 15 minutes. Short enough to be boring. DEFAULT_AGENT_TTL = 900 # 15 minutes. Short enough to be boring.
def hash_token(token: str) -> str:
"""Digest a bearer token for storage.
Session tokens are stored hashed, like one-time codes. A stolen database
must not hand an attacker live sessions; the plaintext token exists only in
the caller's hands, which is where a bearer token belongs.
"""
return hashlib.sha256(token.encode("utf-8")).hexdigest()
def _token() -> str: def _token() -> str:
return secrets.token_urlsafe(32) return secrets.token_urlsafe(32)
@ -59,7 +71,8 @@ def issue_agent_session(
token = _token() token = _token()
expires = int(time.time()) + ttl expires = int(time.time()) + ttl
store.put_session( store.put_session(
token, AGENT, agent, agent, granted, expires, granted_by=None hash_token(token), AGENT, agent, agent, granted, expires,
granted_by=None, fingerprint=fingerprint,
) )
store.record( store.record(
"session.issued", "session.issued",
@ -88,6 +101,11 @@ def issue_view_session(
) -> dict: ) -> dict:
"""Mint a human_via_agent session. """Mint a human_via_agent session.
Plumbing primitive, not an entry point: it trusts whoever calls it. Code
reached from outside the process must authenticate first. issue_code()
enforces that by requiring an agent session, and redeem_code() may present a
one-time code as its credential.
`granted_by` is the agent that approved the code. It is recorded so the `granted_by` is the agent that approved the code. It is recorded so the
audit trail can answer "who let this human in" and not merely "which audit trail can answer "who let this human in" and not merely "which
agent's things did they see". agent's things did they see".
@ -101,7 +119,7 @@ def issue_view_session(
token = _token() token = _token()
expires = int(time.time()) + ttl expires = int(time.time()) + ttl
store.put_session( store.put_session(
token, hash_token(token),
HUMAN_VIA_AGENT, HUMAN_VIA_AGENT,
f"{agent}'s human", f"{agent}'s human",
agent, agent,
@ -134,22 +152,22 @@ def issue_view_session(
def resolve(store: Store, token: str) -> dict: def resolve(store: Store, token: str) -> dict:
"""Look up a session and reject it if revoked or expired.""" """Look up a session and reject it if revoked or expired."""
row = store.get_session(token) row = store.get_session(hash_token(token))
if row is None: if row is None:
raise errors.SessionUnknown("no such session") raise errors.SessionUnknown("no such session")
if row["revoked_at"] is not None: if row["revoked_at"] is not None:
raise errors.SessionExpired("session revoked") raise errors.SessionExpired("session revoked")
if time.time() >= row["expires_at"]: if time.time() >= row["expires_at"]:
raise errors.SessionExpired("session expired") raise errors.SessionExpired("session expired")
import json as _json
return { return {
"token": token, "token": token,
"kind": row["kind"], "kind": row["kind"],
"subject": row["subject"], "subject": row["subject"],
"on_behalf_of": row["on_behalf_of"], "on_behalf_of": row["on_behalf_of"],
"granted_by": row["granted_by"], "granted_by": row["granted_by"],
"scopes": _json.loads(row["scopes"]), "fingerprint": row["fingerprint"],
"revoked_reason": row["revoked_reason"],
"scopes": json.loads(row["scopes"]),
"expires_at": row["expires_at"], "expires_at": row["expires_at"],
} }
@ -179,5 +197,5 @@ def assert_may_act_as_agent(session: dict) -> str:
def revoke(store: Store, token: str) -> None: def revoke(store: Store, token: str) -> None:
store.revoke_session(token) store.revoke_session(hash_token(token))
store.record("session.revoked", actor="system", detail={"token_prefix": token[:8]}) store.record("session.revoked", actor="system", detail={"token_prefix": token[:8]})