Files

117 lines
3.5 KiB
Python
Raw Permalink Normal View History

"""Electronic signatures: a step-up check, then an append-only `signatures` row and a `sig.signed` audit row.
Later phases call `sign` from every sign-off action. There is no HTTP endpoint for it in phase 1.
"""
from __future__ import annotations
import hashlib
import json
from dataclasses import dataclass
from datetime import datetime
from typing import Any
from .. import audit
from ..settings import Settings, get_settings
from . import stepup
from .ldap import DirectoryProtocol
from .sessions import Actor
@dataclass(frozen=True)
class Signature:
id: int
at: datetime
user_id: Any
session_id: Any
account: str
display_name: str
session_kind: str
factor: str
action: str
meaning: str | None
packet_id: int | None
subject_type: str | None
subject_id: str | None
content_hash: bytes | None
def canonical_json(content: Any) -> bytes:
return json.dumps(content, sort_keys=True, separators=(",", ":"), ensure_ascii=False, default=str).encode("utf-8")
def content_hash(content: Any) -> bytes | None:
"""sha256 of the canonical JSON of what was signed; None when nothing was given."""
return None if content is None else hashlib.sha256(canonical_json(content)).digest()
def sign(
conn: Any,
directory: DirectoryProtocol,
session: Actor,
*,
factor: str,
secret: str,
action: str,
meaning: str | None,
packet_id: int | None = None,
subject_type: str | None = None,
subject_id: Any = None,
content: Any = None,
ip: str | None = None,
settings: Settings | None = None,
) -> Signature:
"""Check the PIN or password (raises ApiError when wrong), then record the signature."""
s = settings or get_settings()
stepup.verify(conn, directory, s, session, factor=factor, secret=secret, kind="signature", ip=ip)
h = content_hash(content)
sid = None if subject_id is None else str(subject_id)
with conn.transaction():
row = conn.execute(
"INSERT INTO signatures (user_id, session_id, account, display_name, session_kind, factor, action,"
" meaning, packet_id, subject_type, subject_id, content_hash)"
" VALUES (%s, %s, %s, %s, %s::session_kind, %s::sig_factor, %s, %s, %s, %s, %s, %s) RETURNING id, at",
(
session.user_id,
session.session_id,
session.sam_account,
session.display_name,
session.kind,
factor,
action,
meaning,
packet_id,
subject_type,
sid,
h,
),
).fetchone()
audit.append(
conn,
action="sig.signed",
user_id=session.user_id,
session_id=session.session_id,
role_snapshot=session.role_snapshot,
subject_type=subject_type,
subject_id=sid,
packet_id=packet_id,
detail={"signature_id": int(row[0]), "action": action, "factor": factor},
ip=ip,
)
return Signature(
id=int(row[0]),
at=row[1],
user_id=session.user_id,
session_id=session.session_id,
account=session.sam_account,
display_name=session.display_name,
session_kind=session.kind,
factor=factor,
action=action,
meaning=meaning,
packet_id=packet_id,
subject_type=subject_type,
subject_id=sid,
content_hash=h,
)