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