From 128a8db7638dbc4af3f30bdce2a2601d30631eaf Mon Sep 17 00:00:00 2001 From: bigpickle Date: Mon, 5 Oct 2026 21:24:43 +0800 Subject: [PATCH] Harden Sigil: sigil/sessions.py --- sigil/sessions.py | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/sigil/sessions.py b/sigil/sessions.py index 8ce6e31..9282dfb 100644 --- a/sigil/sessions.py +++ b/sigil/sessions.py @@ -17,6 +17,8 @@ Two properties worth stating out loud: from __future__ import annotations +import hashlib +import json import secrets import time 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. +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: return secrets.token_urlsafe(32) @@ -59,7 +71,8 @@ def issue_agent_session( token = _token() expires = int(time.time()) + ttl 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( "session.issued", @@ -88,6 +101,11 @@ def issue_view_session( ) -> dict: """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 audit trail can answer "who let this human in" and not merely "which agent's things did they see". @@ -101,7 +119,7 @@ def issue_view_session( token = _token() expires = int(time.time()) + ttl store.put_session( - token, + hash_token(token), HUMAN_VIA_AGENT, f"{agent}'s human", agent, @@ -134,22 +152,22 @@ def issue_view_session( def resolve(store: Store, token: str) -> dict: """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: raise errors.SessionUnknown("no such session") if row["revoked_at"] is not None: raise errors.SessionExpired("session revoked") if time.time() >= row["expires_at"]: raise errors.SessionExpired("session expired") - import json as _json - return { "token": token, "kind": row["kind"], "subject": row["subject"], "on_behalf_of": row["on_behalf_of"], "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"], } @@ -179,5 +197,5 @@ def assert_may_act_as_agent(session: dict) -> str: 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]}) \ No newline at end of file