Files
Maven/internal/auth/tier.go
T
2026-07-03 00:32:48 +02:00

95 lines
4.5 KiB
Go

// Package auth is maven's authority layer — the 4-layer cascade and the
// "surface caps authority" invariant.
//
// Spec contract (from maven.md § auth):
//
// a cascade, not a pick-one — each layer answers a different question:
//
// | layer | question | mechanism | surface |
// | 0 | on the network at all? | wireguard | floor |
// | 1 | enrolled box? | mTLS client cert | pc client, authed page |
// | 2 | you, this session? | passkey / webauthn | pc client, authed page |
// | 3 | you, right now, for this act? | passkey user-verification | step-up acts |
//
// wg is necessary-not-sufficient: an unlocked laptop inside the tunnel is
// "authed" at layer 0 only — that gap is why the upper layers exist.
//
// the invariant: auth tier is a property of the SURFACE; the surface caps
// maximum authority. you can't step up past what the channel structurally
// carries. voice STOPS at L0 (a room mic is reachable by anyone present →
// speaker verification is attribution, not auth). telegram inbound = weak
// tier (read + soft acts, never destructive, never registration). only
// pc_client / authed_page carry passkey user-verification (L3) at all.
//
// "compromised X can't forge Y" is the through-line — applied at smaller
// and smaller scope (network → box → process). the auth layer applies it at
// the process radius: a module calling CoreAPI is bound to a Surface; the
// Surface caps what methods/authority that call can carry.
package auth
// Layer — one rung of the auth cascade.
type Layer int8
const (
// Layer0 — wireguard: on the network at all. Floor for everything.
Layer0 Layer = 0
// Layer1 — mTLS client cert: enrolled box. Optional per spec ("if dropping
// one, drop mTLS, never the passkey"); not wired by the floor enrollment.
Layer1 Layer = 1
// Layer2 — passkey/webauthn session: you, this session.
Layer2 Layer = 2
// Layer3 — passkey user-verification gesture for a SINGLE act: you, right
// now, for this. registration-enable, destructive acts, core cold-start
// unlock. the highest-authority op.
Layer3 Layer = 3
)
// Surface — where a call ENTERS maven from. A property of the channel that
// structurally caps the maximum authority that channel can carry. The wire
// doesn't carry a layer — it carries a Caller; the Enrollment maps the caller
// to a Surface; MaxLayer caps it. So voice can never reach EnableTool — not
// because auth "failed" but because the channel can't carry the proof.
type Surface string
const (
// SurfaceVoice — a room mic / wake-word path. Speaker verification is
// attribution, not auth: anyone present (gf, the TV) can speak. STOPS at
// L0. never destructive, never registration.
SurfaceVoice Surface = "voice"
// SurfaceTelegram — telegram inbound. weak tier: telegram's own auth,
// outside our control. read + soft acts, never destructive, never
// registration. carries up to L2 (a chat-id allowlist is the best we get).
SurfaceTelegram Surface = "telegram"
// SurfacePCClient — the desktop gui, mTLS'd and passkey'd. carries L3
// (passkey user-verification is available on-device).
SurfacePCClient Surface = "pc_client"
// SurfaceAuthedPage — the web authed page over wg. carries L3 (passkey
// user-verification via the browser / platform authenticator).
SurfaceAuthedPage Surface = "authed_page"
// SurfaceCoreProcess — a module running in core's own address space (the
// daemon-embedded router/delivery today). There is no boundary to cross;
// this is the 0600-floor equivalent: trusted same-process. Layer3-capped
// since the user already unlocked the daemon (cold-start IS L3 per spec).
SurfaceCoreProcess Surface = "core_process"
// SurfaceUnknown — enrollment didn't recognize the caller. fail closed.
SurfaceUnknown Surface = "unknown"
)
// MaxLayer — the surface-caps-authority table. PURE. The invariant: you
// cannot step up past what your channel structurally carries. voice → L0;
// telegram → L2 (chat allowlist); pc_client / authed_page / core_process →
// L3. An unrecognized surface caps at -1 (fail closed) — a caller with no
// enrolled identity is not "unauthed at L0", it's refused outright.
func MaxLayer(s Surface) Layer {
switch s {
case SurfaceVoice:
return Layer0
case SurfaceTelegram:
return Layer2
case SurfacePCClient, SurfaceAuthedPage, SurfaceCoreProcess:
return Layer3
default:
return -1
}
}