Harden Sigil: sigil/sessions.py
This commit is contained in:
parent
dc55b9a7aa
commit
fba91c1bf7
@ -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]})
|
||||
Loading…
x
Reference in New Issue
Block a user