gpgsmith

package
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 11, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package gpgsmith is the gpgsmith kernel: orchestration, sessions, and the types frontends (CLI, web UI, TUI) build on. It sits above the primitive packages (pkg/gpg, pkg/vault, pkg/audit) and below the frontend packages (pkg/cli/gpgsmith, pkg/webui/gpgsmith, pkg/tui/gpgsmith).

Index

Constants

View Source
const (

	// HeartbeatInterval is how often a running daemon refreshes the .info
	// liveness timestamp on disk.
	HeartbeatInterval = 30 * time.Second

	// StaleHeartbeatThreshold is how long ago the last heartbeat must be
	// before another process treats the session as stale (presumed crashed
	// or otherwise dead). Generously larger than HeartbeatInterval to
	// tolerate sync delay and load spikes.
	StaleHeartbeatThreshold = 90 * time.Second

	// EphemeralStatusActive marks a session whose daemon is presumed to be
	// running and heartbeating.
	EphemeralStatusActive EphemeralStatus = "active"

	// EphemeralStatusIdleSealed marks a session that was auto-sealed by the
	// idle timer; the encrypted state file is on disk and the in-memory
	// daemon state has been dropped.
	EphemeralStatusIdleSealed EphemeralStatus = "idle-sealed"
)
View Source
const (
	// DefaultHeartbeatInterval is the production tick rate for the .info
	// liveness sidecar. Tests pass a smaller value via SessionOpts.
	DefaultHeartbeatInterval = HeartbeatInterval
)

Variables

This section is empty.

Functions

func DeleteEphemeralFiles added in v0.4.0

func DeleteEphemeralFiles(statePath, infoPath string) error

DeleteEphemeralFiles removes the .session-<host> and .info file pair. Either or both may be absent; missing files are not errors.

func HardenProcess added in v0.4.0

func HardenProcess() error

HardenProcess applies process-level defenses against same-user attackers trying to read the daemon's heap or files via debugger or /proc.

On Linux:

  • PR_SET_DUMPABLE = 0 blocks ptrace, process_vm_readv, /proc/<pid>/{mem, maps,root,cwd} from non-root processes (the kernel re-owns these to root once dumpable=0)
  • RLIMIT_CORE = 0,0 prevents core dumps from leaking heap on crash

On macOS:

  • PT_DENY_ATTACH blocks ptrace attach (the macOS analog of PR_SET_DUMPABLE for debugger denial)
  • RLIMIT_CORE = 0,0 prevents core dumps

HardenProcess is intended to be called once at daemon startup, before any secret material is touched. It is safe to call multiple times. It returns an error only if the syscalls fail in a way that suggests the host kernel is broken; routine "operation not permitted" returns are downgraded to a silent no-op so test environments and unusual configurations don't break the daemon.

HardenProcess is NOT called automatically by Session or any other kernel API. The daemon binary's main() is responsible for calling it explicitly, because:

  1. Hardening is a process-wide flag — it would affect tests that import the kernel package if it ran in package init.
  2. Setting PR_SET_DUMPABLE=0 makes a process non-attachable by gdb, which is friction during development. The daemon binary opts in; tests and non-daemon callers do not.
  3. The daemon may want to set other rlimits or prctls beyond the defaults; surfacing this as an explicit call lets it compose with future tightening.

To opt out (for debugging) the daemon main() can simply not call this. We do not provide an environment-variable opt-out because the call site is in the daemon's own main() — adding env logic there is the user of this function's choice, not ours.

func IsMasterKeyMismatch added in v0.4.0

func IsMasterKeyMismatch(err error) bool

IsMasterKeyMismatch reports whether err is a MasterKeyMismatchError.

func ParseSessionFilename added in v0.4.0

func ParseSessionFilename(name string) (canonicalBase, hostname string, ok bool)

ParseSessionFilename extracts the canonical base and hostname from a .session-<hostname> filename. Returns ok=false if the name does not match the expected pattern. Accepts both bare names and full paths; the directory portion is ignored.

func SessionFilenamesFor added in v0.4.0

func SessionFilenamesFor(canonicalBase, hostname string) (statePath, infoPath string)

SessionFilenamesFor returns the (state, info) filenames for a session held by the given hostname against the given canonical snapshot base filename. hostname must be the bare host name (no dots stripped, no munging beyond what os.Hostname returns).

func WriteEphemeralInfo added in v0.4.0

func WriteEphemeralInfo(path string, info *EphemeralInfo) error

WriteEphemeralInfo serializes info to YAML and writes it atomically to path. The write goes to a temp file followed by rename so observers never see a half-written file.

Types

type Ephemeral added in v0.4.0

type Ephemeral struct {
	// VaultDir is the absolute path of the vault directory holding the
	// session files.
	VaultDir string

	// CanonicalBase is the filename of the canonical snapshot this
	// session was opened from (e.g. "20260410T143012Z_setup.tar.age").
	CanonicalBase string

	// SessionPath is the full path to the encrypted state file
	// (<vaultdir>/<canonical>.session-<host>). May be empty if the
	// session was opened but never wrote any state to disk yet
	// (the .info exists alone).
	SessionPath string

	// InfoPath is the full path to the .info sidecar.
	InfoPath string

	// Info is the parsed .info contents.
	Info EphemeralInfo
}

Ephemeral describes an in-progress session file pair (.session-host and its .info sidecar) discovered in a vault directory. It does NOT include any decrypted state — only the on-disk filenames and the plaintext .info contents.

func FindEphemeralFor added in v0.4.0

func FindEphemeralFor(vaultDir, hostname string) (*Ephemeral, error)

FindEphemeralFor returns the Ephemeral matching the given hostname in vaultDir, or nil if none exists.

func ListEphemerals added in v0.4.0

func ListEphemerals(vaultDir string) ([]Ephemeral, error)

ListEphemerals scans vaultDir for all .session-<hostname>.info files and returns the parsed Ephemeral records. Files whose .info cannot be parsed are silently skipped (they're treated as junk; the caller can decide what to do with them). The returned slice is sorted by hostname for stable output.

func (*Ephemeral) IsDivergent added in v0.4.0

func (e *Ephemeral) IsDivergent(canonicalNames []string) bool

IsDivergent reports whether the canonical the ephemeral was based on is older than the latest canonical present in the same vault directory. Returns true when newer canonicals exist (the user has changes from elsewhere that the ephemeral does not include).

canonicalNames is the list of canonical snapshot filenames currently in the vault dir, in arbitrary order. This is supplied by the caller (rather than read here) so the function is testable in isolation.

type EphemeralInfo added in v0.4.0

type EphemeralInfo struct {
	Hostname      string          `yaml:"hostname"`
	Source        LockSource      `yaml:"source"`
	StartedAt     time.Time       `yaml:"started_at"`
	LastHeartbeat time.Time       `yaml:"last_heartbeat"`
	Generation    uint64          `yaml:"generation"`
	Status        EphemeralStatus `yaml:"status"`
}

EphemeralInfo is the plaintext content of a .session-<host>.info file. It is updated on every heartbeat tick and read by other processes (other gpgsmith invocations on the same or different hosts) without needing the vault passphrase.

func ReadEphemeralInfo added in v0.4.0

func ReadEphemeralInfo(path string) (*EphemeralInfo, error)

ReadEphemeralInfo reads and parses a .info sidecar file.

func (*EphemeralInfo) IsIdleSealed added in v0.4.0

func (e *EphemeralInfo) IsIdleSealed() bool

IsIdleSealed reports whether the ephemeral was put to rest by the idle timer (as opposed to actively crashed or actively running).

func (*EphemeralInfo) IsStale added in v0.4.0

func (e *EphemeralInfo) IsStale(now time.Time) bool

IsStale reports whether the last heartbeat is older than the threshold. Used to detect crashed daemons whose .info file remains on disk.

type EphemeralStatus added in v0.4.0

type EphemeralStatus string

EphemeralStatus is the lifecycle state recorded in a .info sidecar.

type LockSource added in v0.4.0

type LockSource string

LockSource identifies which gpgsmith frontend is driving an open session. Before the daemon refactor this identifier tagged a flock acquisition; in the daemon era it is recorded in the encrypted ephemeral .info sidecar for diagnostic purposes only.

const (
	// LockSourceCLI marks a session opened by the CLI frontend.
	LockSourceCLI LockSource = "cli"
	// LockSourceUI marks a session opened by the local web UI.
	LockSourceUI LockSource = "ui"
	// LockSourceTUI marks a session opened by the terminal UI (future).
	LockSourceTUI LockSource = "tui"
)

type MasterKeyMismatchError added in v0.4.0

type MasterKeyMismatchError struct {
	VaultName string
	Expected  string // from vault.Entry.TrustedMasterFP
	Found     string // from gpgsmith.yaml inside the decrypted snapshot
	Snapshot  string // basename of the snapshot the embedded fp was read from
}

MasterKeyMismatchError is returned by OpenSession (and ResumeSession) when the master fingerprint embedded in the decrypted vault does not match the trusted fingerprint recorded in the vault registry. This is the loud security signal: either an attacker substituted the snapshot or the user rotated their master key without updating the trust anchor.

func (*MasterKeyMismatchError) Error added in v0.4.0

func (e *MasterKeyMismatchError) Error() string

Error implements the error interface.

type OpenSessionResult added in v0.4.0

type OpenSessionResult struct {
	Session *Session

	// TOFUFingerprint is non-empty when this is the first time we've
	// seen a master fingerprint for this vault and the caller should
	// persist it into the vault registry's TrustedMasterFP field. It
	// is empty when:
	//   - The Entry already had a TrustedMasterFP and it matched.
	//   - The decrypted vault has no gpgsmith.yaml yet (no master key
	//     generated; nothing to TOFU on).
	TOFUFingerprint string
}

OpenSessionResult is returned by OpenSession. It carries the Session and side-channel information about TOFU first-use, which the caller is responsible for persisting back to the vault registry config.

func OpenSession added in v0.4.0

func OpenSession(
	ctx context.Context,
	v *vault.Vault,
	entry *vault.Entry,
	opts SessionOpts,
) (*OpenSessionResult, error)

OpenSession decrypts the latest canonical snapshot of the resolved vault entry and prepares an in-memory Session ready for operations. It:

  1. Calls v.Open(ctx) to decrypt the latest snapshot into a tmpfs workdir
  2. Writes gpg.conf and gpg-agent.conf into the workdir for loopback pinentry mode
  3. Constructs a gpg.Client wired to the workdir and the vault passphrase
  4. Reads the embedded gpgsmith.yaml and performs TOFU on master_fp: - If entry.TrustedMasterFP is empty, populate it (returned in OpenSessionResult.TOFUFingerprint) - If non-empty and matches, OK - If non-empty and mismatches, refuse with MasterKeyMismatchError
  5. Computes the ephemeral file paths from the source canonical filename and the local hostname
  6. Writes the initial .info sidecar with status=active and the current heartbeat timestamp
  7. Starts the heartbeat goroutine

On any failure after the workdir is decrypted, the workdir is removed and any partially-written ephemeral files are cleaned up.

func ResumeSession added in v0.4.0

func ResumeSession(
	ctx context.Context,
	v *vault.Vault,
	entry *vault.Entry,
	eph *Ephemeral,
	opts SessionOpts,
) (*OpenSessionResult, error)

ResumeSession is the resume-from-ephemeral counterpart to OpenSession.

Where OpenSession decrypts the latest canonical snapshot, ResumeSession decrypts the previously-flushed .session-<host> ephemeral state file pointed to by eph and uses that as the workdir. This recovers an in-progress session that was put to rest by AutoSealAndDrop or by an earlier crash, restoring the exact mutation state that was on disk at the time of the last heartbeat-flush.

On success the on-disk ephemeral file pair is deleted (the new in-memory session takes over ownership of those bytes), the .info sidecar is rewritten in active state, and the heartbeat goroutine is started.

On failure no ephemeral files are touched, so a subsequent attempt can retry the resume.

type Session added in v0.4.0

type Session struct {
	Vault    *vault.Vault
	Entry    *vault.Entry
	Source   LockSource
	Hostname string
	Logger   *slog.Logger

	Workdir       string
	GPG           *gpg.Client
	SourceSnap    *vault.Snapshot // the canonical the session was opened from
	StartedAt     time.Time
	CanonicalBase string // base filename of SourceSnap; used to derive ephemeral paths

	// TOFU result. ConfiguredMasterFP is the master_fp read from the
	// decrypted gpgsmith.yaml inside the workdir, or "" if no key has
	// been generated yet.
	ConfiguredMasterFP string
	// contains filtered or unexported fields
}

Session is the in-memory representation of an open vault. It owns the decrypted GNUPGHOME workdir, an authenticated GPG client, the heartbeat goroutine that keeps the on-disk liveness sidecar fresh, and a mutation generation counter that drives periodic re-flush of the encrypted .session-<host> ephemeral state file.

A Session is created via OpenSession (open the latest canonical) or ResumeSession (resume from an existing .session-<host> ephemeral). It is ended via Seal (write a new canonical) or Discard (throw away the work). Both end-paths stop the heartbeat goroutine, delete the on-disk ephemeral pair, and remove the workdir from /dev/shm.

AutoSealAndDrop is a third end-path triggered by an idle timeout. It flushes the workdir to the encrypted ephemeral file, drops the in-memory state, and stops heartbeating — but leaves the .session-<host> and .info files on disk so the next OpenSession on the same vault can offer to resume.

Session is safe for concurrent use by multiple goroutines: mutation counters are atomic, the heartbeat goroutine never reads or writes the workdir on its own, and Seal/Discard are protected by an internal mutex.

func (*Session) AutoSealAndDrop added in v0.4.0

func (s *Session) AutoSealAndDrop(ctx context.Context) error

AutoSealAndDrop is the idle-timeout end-path. It does NOT write a new canonical snapshot — the work-in-progress remains in the encrypted .session-<host> ephemeral file on disk, ready for the next OpenSession on this vault to offer as a resume option.

Specifically, AutoSealAndDrop:

  1. Forces a final flush of the workdir to the encrypted ephemeral file
  2. Marks the .info sidecar status as idle-sealed
  3. Stops the heartbeat goroutine
  4. Removes the workdir from tmpfs
  5. Marks the Session as closed (any further method call returns an error)

The .session-<host> and .info files remain on disk. The next attempt to open the vault will detect them and prompt to resume.

func (*Session) Discard added in v0.4.0

func (s *Session) Discard(ctx context.Context) error

Discard explicitly throws away the workdir and the ephemeral file pair without writing a new canonical snapshot. The per-session gpg-agent and scdaemon are killed before the workdir is removed so they don't become orphans pointing at a non-existent directory. After Discard returns, the Session is unusable.

func (*Session) Generation added in v0.4.0

func (s *Session) Generation() uint64

Generation returns the current mutation generation. Useful for tests.

func (*Session) IsClosed added in v0.4.0

func (s *Session) IsClosed() bool

IsClosed reports whether the Session has been ended via Seal, Discard, or AutoSealAndDrop.

func (*Session) MarkChanged added in v0.4.0

func (s *Session) MarkChanged()

MarkChanged increments the mutation generation counter. The next heartbeat tick will detect the change and re-flush the workdir to the encrypted .session-<host> ephemeral file. Mutation operations on the kernel API (key generation, identity add, etc.) call this after a successful change.

Safe to call from any goroutine.

func (*Session) Seal added in v0.4.0

func (s *Session) Seal(ctx context.Context, message string) (*vault.Snapshot, error)

Seal explicitly writes the workdir as a new canonical snapshot, deletes the ephemeral file pair, stops the heartbeat goroutine, kills the per-session gpg-agent and scdaemon, and frees all in-memory state. After Seal returns, the Session is unusable.

type SessionOpts added in v0.4.0

type SessionOpts struct {
	// Source identifies which frontend is opening this session.
	Source LockSource

	// Logger receives kernel-level structured logs. Defaults to
	// slog.Default().
	Logger *slog.Logger

	// HeartbeatInterval overrides the default heartbeat tick. Mainly
	// for tests; production callers should leave this zero (uses
	// DefaultHeartbeatInterval).
	HeartbeatInterval time.Duration
}

SessionOpts configures how a session is opened.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL