secrets

package
v1.2.9 Latest Latest
Warning

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

Go to latest
Published: Oct 11, 2026 License: MIT Imports: 31 Imported by: 0

Documentation

Overview

Package secrets implements the credential-layer machinery described in docs/SPEC-SECRETS.md. It provides four verbs -- exec, names, check, and keygen -- as a thin wrapper over sops and age-keygen, linking no cryptography of its own.

THE ONE RULE THIS PACKAGE EXISTS TO KEEP: a credential value is never printed, logged, put in an argument list, or written anywhere but the child process environment that asked for it by name.

Index

Constants

View Source
const DecisionKey = "JEV_API_KEY"

DecisionKey is the secret the sprint server's decision loop reads. It is the same name decide.JevSecret carries; secrets does not import that package.

View Source
const KeygenNextLine = "SECRETS RULE NEXT: add these two lines to .sops.yaml (or run `nova-secrets seat add`)"

KeygenNextLine is the one line that tells the reader what is left to do in the rule block. It is a NEXT STEP and says so: the same text as a state of the world ("the placeholder stands unfilled") read as a failure at the end of a green run.

View Source
const MinAgeKeygenVersion = "1.3.2"
View Source
const MinLeakFragment = 6

MinLeakFragment is how many consecutive bytes of a secret count as a leak. Six, not "the whole value", because the leak that actually happens is a truncated one.

View Source
const MinSopsVersion = "3.13.3"
View Source
const Redacted = "[redacted]"

Redacted is the only string a Secret ever yields to a formatter. One word, in one place, so no caller can produce a "helpfully" partial one.

View Source
const SeatMarkKey = "NOVA_SECRETS_WRITTEN_BY"

SeatMarkKey is the root key every verb-written seat file carries in the clear. A rule that lists it in `unencrypted_regex` keeps sops from sealing it, and the gate reads it.

Variables

View Source
var MarkVersion = "dev"

MarkVersion is the tool version the mark names; the command sets it from its build stamp.

Functions

func CheckAgeKeygenVersion

func CheckAgeKeygenVersion(run execCommand, ageKeygenPath string) (string, error)

CheckAgeKeygenVersion probes the age-keygen binary.

func CheckInvariant6

func CheckInvariant6(keyPath string) error

CheckInvariant6 verifies that the key file mode is 0600 and directory is 0700.

func CheckSopsVersion

func CheckSopsVersion(run execCommand, sopsPath string) (string, error)

CheckSopsVersion probes the sops binary with --disable-version-check to prevent network calls.

func DecryptFile

func DecryptFile(run execCommand, sopsPath, keyPath, filePath string) ([]byte, error)

DecryptFile invokes sops -d on a file using an isolated environment. It sets SOPS_AGE_KEY_FILE to keyPath, strips all other SOPS_* variables, and sets HOME and XDG_CONFIG_HOME to an empty temporary directory.

func ExtractPublicKeyFromKeyFile

func ExtractPublicKeyFromKeyFile(keyPath string) (string, error)

ExtractPublicKeyFromKeyFile parses '# public key: (age1...)' from an age private key file.

func GitBlobSHA1

func GitBlobSHA1(content []byte) [20]byte

GitBlobSHA1 computes the SHA1 hash of a git blob object for content.

func IsValidAgePublicKey

func IsValidAgePublicKey(s string) bool

IsValidAgePublicKey verifies an age public key string (bech32).

func IsValidAsName

func IsValidAsName(name string) bool

IsValidAsName checks if a seat name matches [A-Za-z0-9_-]+

func IsValidEnvVar

func IsValidEnvVar(s string) bool

IsValidEnvVar verifies an environment variable name: ^[A-Z][A-Z0-9_]*$

func Leaks

func Leaks(text string, s Secret) bool

Leaks reports whether text contains this secret, whole or in a fragment of at least MinLeakFragment consecutive bytes.

func ParseDecryptedSecrets

func ParseDecryptedSecrets(data []byte) (map[string]Secret, []string, error)

ParseDecryptedSecrets parses the output of 'sops -d' into Secret values. Rejects multi-line values, NUL bytes, and illegal variable names.

func ReadFleetMachines

func ReadFleetMachines(path string) (map[string]FleetMachine, error)

ReadFleetMachines parses the place machine table: name, ssh target, home, and the optional fourth column the pulse fleet file carries. It refuses the gate registry's seven-column format rather than treating os/arch as a home path (SPEC-SECRETS "place"). Blank lines and `#` comments are skipped; a line with fewer than two fields, more than four fields, or an empty name or target is refused.

func ReadHEADTreeBlobs

func ReadHEADTreeBlobs(storeDir string) (map[string]string, error)

ReadHEADTreeBlobs reads the tree of the HEAD commit and returns a map of relative path -> blob SHA1. It inspects loose objects directly in pure Go, with fallback to git ls-tree if needed.

func ReadRecoveryPub

func ReadRecoveryPub(storeDir string) (string, error)

ReadRecoveryPub reads and validates the recovery.pub file at store root.

func ReadUnitKeys

func ReadUnitKeys(u UnitKeyLogin) (map[string]Secret, error)

ReadUnitKeys reads each named secret in this process through OpenSeatFile, once. The values come back as Secrets and go into no environment. A field left empty, a name that is not a secret name, a seat that does not open, and a name the seat does not hold or holds empty are each a refusal naming that name and the next command; none of them is ever an empty key.

func RouteKey

func RouteKey(model string, names []string) string

RouteKey is the one name of names a route needs. The provider of model (provider/model, the slash optional) wants <PROVIDER>_API_KEY when that name is listed. Otherwise the only listed name is the route's key. The decision key is never a route key. "" when none of that is so: more than one name and none is the route's, which is not the whole set.

func RunCheck

func RunCheck(storeDir, asName, keyPath, sopsPath string, maxShown int) (okLine string, failLines []string, moreLines []string, summaryLine string, exitCode int, err error)

RunCheck executes the complete verification suite for 'check'.

func RunExec

func RunExec(storeDir, asName, keyPath, sopsPath, onlyArg string, required []string, cmdArgs []string) (int, error)

RunExec validates all preflight invariants, decrypts <store>/<as>.yaml, applies --only and --require filters, prints the OK line to stderr, sets RLIMIT_CORE to 0, and replaces the current process with cmdArgs. The OK line goes out before the exec call, so a failure of that call returns 125 with a FAIL line saying the command never started.

func RunGate

func RunGate(in GateInput) (string, int)

RunGate is the seat-rule gate as a verb: the store's shell gate, called by the workflow. It diffs --base..--head with git (no GitHub) and either prints "GATE APPROVE files=<n> machines=<registry|->" at exit 0 or "GATE FAILED rule=<n> check=<k> file=<f>: <why>" at exit 1; a gate that could not run prints "SECRETS GATE REFUSED: <why>; run: nova-secrets gate -h" at exit 2, so a CI step reads a broken change apart from a gate that never ran (skeleton contract 1.2). Independent inputs and findings are reported together in the check order (SPEC-SECRETS "gate").

func RunKeygen

func RunKeygen(asName, keyPath, ageKeygenPath, storeDir string) (lines []string, err error)

RunKeygen generates a new age private key and formats the .sops.yaml rule block. It returns the receipt as ordered lines; the caller prints them in that order.

func RunPlace

func RunPlace(in PlaceInput) (string, error)

RunPlace copies one secret to one machine and records a receipt. It returns the one OK line, or an error whose text is safe to print (it never contains the value).

func RunPlaced

func RunPlaced(in PlacedInput) (string, []string, error)

RunPlaced lists the receipts written for one machine: each secret, its remote path, the sealed file it was placed from (file, head, blob) and its stamp. A receipt from an older build lists as identity=unknown, and a NOTE says its file still holds an old digest on disk and how to rewrite or remove it.

func RunSeal

func RunSeal(opts SealOptions) (line string, err error)

RunSeal reads one value, folds it into the seat file under --name, and carries the change through a branch, a commit and, unless --no-pr, a pull request to its merge.

func RunSeatAdd

func RunSeatAdd(opts SeatAddOptions) ([]string, error)

RunSeatAdd re-seals the named values out of a source seat into a new seat's file and writes that seat's rule. It returns the receipt as ordered lines, the verdict last. No value appears on any of them, ever.

func RunSeatInject

func RunSeatInject(opts SeatInjectOptions) (string, error)

RunSeatInject re-seals the --only values out of the source seat into an existing seat's file, encrypted to that file's own recipients, and carries the change to the store as `seal` does. It returns the one OK line; no value appears on it, ever.

func ValidateAdmissibleStore

func ValidateAdmissibleStore(storeDir string) (status GitRefStatus, headBlobs map[string]string, indexData *GitIndexData, failures []CheckFailure, refusal error)

ValidateAdmissibleStore verifies that the git working copy at storeDir is an admissible store. It verifies: 1. Invariant 8 ref tracking: HEAD equals remote-tracking ref (via CheckGitWorkingCopy). 2. HEAD commit tree and git index can be read. 3. Every tracked store file (*.yaml, .sops.yaml, recovery.pub) in HEAD tree or git index:

  • exists in working copy as a regular file (no deletions or non-regular artifact replacements)
  • is tracked in index and committed in HEAD tree (no uncommitted additions or index drift)
  • working copy blob SHA1 matches HEAD tree blob SHA1 and git index blob SHA1 (no unstaged or staged modifications)

Types

type CheckFailure

type CheckFailure struct {
	Kind   string // invariant kind: "rule-shape", "recipients-drift", "unsealed", "decrypt-failed", "foreign-openable", "store-private-key", "untracked-plaintext", "stale-working-copy"
	File   string // file path or subject
	Reason string // diagnostic detail
}

CheckFailure records one invariant failure found during 'check'.

func CheckInvariant1

func CheckInvariant1(storeDir string, sopsCfg *SopsConfig, recoveryKey string) []CheckFailure

CheckInvariant1 checks .sops.yaml syntax and shape against the declared recovery key.

func CheckInvariant2

func CheckInvariant2(storeDir string, sopsCfg *SopsConfig, files []string) []CheckFailure

CheckInvariant2 verifies that each file's sops recipient block matches its creation rule.

func CheckInvariant3

func CheckInvariant3(storeDir string, sopsCfg *SopsConfig, files []string) (failures []CheckFailure, sealedCount int, clearCount int)

CheckInvariant3 verifies that every file is sealed and only permitted keys are in the clear.

func CheckInvariant4

func CheckInvariant4(storeDir, sopsPath, keyPath, seatPubKey string, files []string) (failures []CheckFailure, mineCount int, foreignCount int)

CheckInvariant4 tests that the seat key opens exactly the files listing its public key.

func CheckInvariant5

func CheckInvariant5(storeDir, keyPath string) []CheckFailure

CheckInvariant5 verifies that no private key is stored under storeDir.

func CheckInvariant7

func CheckInvariant7(storeDir string, trackedFiles map[string]bool) []CheckFailure

CheckInvariant7 verifies that untracked files hold no plaintext secrets.

type CheckSummary

type CheckSummary struct {
	Recipients int // unique recipient public keys across all rules
	Files      int // count of *.yaml files (excluding .sops.yaml)
	Sealed     int // count of files that are sealed
	Mine       int // count of files whose recipients list this seat
	Foreign    int // count of files whose recipients do not list this seat
	Clear      int // count of keys in the clear under unencrypted_regex
	Failures   []CheckFailure
}

CheckSummary aggregates counts and failures across the store.

type CreationRule

type CreationRule struct {
	PathRegex        string
	UnencryptedRegex string
	Recipients       []string
	NonAgeRecipients []string
}

CreationRule defines one rule in .sops.yaml.

func FindMatchingRule

func FindMatchingRule(cfg *SopsConfig, relPath string) (*CreationRule, error)

FindMatchingRule locates the creation rule that governs filePath.

type FleetMachine

type FleetMachine struct {
	Name   string
	Target string
	Home   string
}

FleetMachine is one machine in the fleet registry: a name, the ssh target that reaches it, and its home directory (used as the default root for a placed secret).

type GateInput

type GateInput struct {
	StoreDir     string
	Base         string
	Head         string
	MachinesPath string // the fleet machines registry; "" leaves the recipient rule dormant
}

GateInput is one call of the gate: the store, the two refs, and -- optionally -- the fleet's machines registry, the only thing that can vouch for a seat nobody has seen before.

type GitIndexData

type GitIndexData struct {
	Entries map[string]GitIndexEntry
}

GitIndexData holds all parsed entries from .git/index.

func ReadGitIndex

func ReadGitIndex(storeDir string) (*GitIndexData, error)

ReadGitIndex reads .git/index directly to find all tracked files and their blob SHAs. Supports index formats v2, v3, and v4 (prefix compression).

type GitIndexEntry

type GitIndexEntry struct {
	Path     string
	BlobSHA1 [20]byte
}

GitIndexEntry holds path and blob SHA1 parsed from .git/index.

type GitRefStatus

type GitRefStatus struct {
	HeadSHA   string
	RemoteRef string
	RemoteSHA string
	Clean     bool
	Refusal   string
}

GitRefStatus holds the result of verifying invariant 8 against a git working copy.

func CheckGitWorkingCopy

func CheckGitWorkingCopy(storeDir string) (GitRefStatus, error)

CheckGitWorkingCopy inspects .git directly without invoking the git binary. It verifies invariant 8: HEAD equals the remote-tracking ref it tracks. Refusals are returned for: - .git is missing or not a directory (a file indicates submodule or worktree) - detached HEAD - branch with no remote tracking upstream

type Login

type Login struct {
	Store string // the store's working copy (--store)
	As    string // the seat (--as)
	Key   string // the seat's age key file (--key)
	Sops  string // the sops binary (--sops)
	Name  string // the secret's name in the seat's file (--secret)
}

Login is where a tool's store login reads its password in its own process: one name in one seat of a secrets store, opened with the seat's key and sops. It holds no secret and cannot: a tool may record a Login in its config file and print every field of it (nova-sprint seat login, docs/SPEC-SECRETS.md, "A tool's store login").

func (Login) Missing

func (l Login) Missing() string

Missing names every field of the login left empty, as the flags that set them, in one list: "" when none is.

func (Login) String

func (l Login) String() string

String is the login on one line, every field a fact and none of them a secret.

func (Login) UnitKeyLogin

func (l Login) UnitKeyLogin(names ...string) UnitKeyLogin

UnitKeyLogin returns a UnitKeyLogin for the seat named by l, naming names.

type NameRow

type NameRow struct {
	Name  string
	Clear bool
}

NameRow is one key name in a seat's file and whether the store keeps it in the clear. It holds no value, sealed or clear.

type NamesReport

type NamesReport struct {
	As, StoreDir         string
	Rows                 []NameRow
	Total, Sealed, Clear int
}

NamesReport is what names found: the seat, the rows shown (at most --max), and the counts over the whole file. Lines renders it as typed lines; the caller renders the same value as JSON, so the two cannot drift.

func RunNames

func RunNames(storeDir, asName string, maxShown int) (NamesReport, error)

RunNames reads <store>/<as>.yaml without decrypting and reports top-level key names.

func (NamesReport) Lines

func (r NamesReport) Lines() (okLine string, nameLines []string, moreLine string)

Lines is the report as the typed lines names prints: one NAME line per row shown, a MORE line when --max cut the list, and the OK line.

type PlaceInput

type PlaceInput struct {
	StoreDir   string
	AsName     string
	KeyPath    string
	SopsPath   string
	Machine    string
	Secret     string
	RemotePath string
	Machines   string
	Receipts   string
	SSH        string
	// DryRun prints the plan and writes nothing: the seat file is decrypted (the read the
	// verb needs to know the secret exists) but no ssh child runs and no receipt is written.
	DryRun bool
	Now    func() time.Time

	// Exec replaces the os/exec child process, as it does for seal: the sops decrypt, the
	// version probe and the ssh delivery all run through it. Tests set a strict fake so
	// place's refusals run with no real sops and no host reached.
	Exec execCommand

	// Guard replaces the process-wide host guard for the ssh delivery. A test passes
	// testguard.NewGuard(true) so it can assert the seam refuses without setting
	// NOVA_TEST_NO_HOST in the process environment, which would race every parallel
	// test. Nil uses testguard's process-wide default, the production path.
	Guard *testguard.Guard
}

PlaceInput is one `nova-secrets place` invocation.

type PlacedInput

type PlacedInput struct {
	Machine  string
	Receipts string
}

PlacedInput is one `nova-secrets placed` invocation.

type SealOptions

type SealOptions struct {
	StoreDir string
	AsName   string
	KeyPath  string
	SopsPath string
	Name     string
	GHPath   string
	GitPath  string

	NoPR     bool
	UseStdin bool

	// DryRun prints the plan and writes nothing: no value is read, nothing is encrypted,
	// no file is written, and no git write, push, gh or sops encrypt runs. The plan is
	// read off the same carry the real run walks (sealCarry), so the two cannot differ.
	DryRun bool

	Stdin           io.Reader
	StdinIsTerminal bool

	// Progress receives one short line per step that can take time (nil = silent).
	// A person at a terminal must be able to tell waiting from hung: any interactive
	// verb says what it is doing before each step that can exceed a blink. The lines
	// go to stderr so the one-line result on stdout stays the whole machine answer.
	Progress io.Writer

	Now   func() time.Time
	Sleep func(time.Duration)
	Check func(storeDir, asName, keyPath, sopsPath string) error

	// Exec replaces the os/exec child process. Tests set it to a pure-Go fake
	// so the seal path runs where a POSIX shell-script fake cannot.
	Exec execCommand
}

SealOptions carries one seal request and the test seams around it.

The value travels from Stdin (or the controlling terminal) to the encrypt child's stdin and nowhere else: not argv, not a file in the clear, not an output line.

type SeatAddOptions

type SeatAddOptions struct {
	StoreDir string
	AsName   string // the new seat
	Pub      string // the new seat's age public key
	From     string // a seat this machine can open
	Only     string // comma-separated key names to carry over
	KeyPath  string // this machine's key: the source seat's identity
	SopsPath string

	// Progress receives one short line per step that can take time (nil = silent).
	// It never carries a value: step names and public facts only.
	Progress io.Writer

	// Exec replaces the os/exec child process, as it does for seal.
	Exec execCommand
}

SeatAddOptions carries one `seat add` request and the test seam around it.

KeyPath is THIS machine's key, the one that opens From. Pub is the NEW seat's public half, which arrives from the new bench's own `keygen` receipt -- never a private key, which never leaves the bench that made it.

type SeatFile

type SeatFile struct {
	Path    string            // <store>/<seat>.yaml
	HeadSHA string            // the store's HEAD the file was read at
	Secrets map[string]Secret // every key in the file, by name
}

SeatFile is one seat's file, opened. Values stay inside Secret: nothing here prints, logs or returns one as a string.

func OpenSeatFile

func OpenSeatFile(storeDir, asName, keyPath, sopsPath string) (SeatFile, error)

OpenSeatFile runs every check exec runs before it decrypts -- the store is a directory working copy with a .sops.yaml and the seat's file, invariant 8 (HEAD equals its upstream, nothing uncommitted or untracked), invariant 1 (recovery.pub and the creation rules), invariant 6 (the key file's mode) and the sops version -- then decrypts <store>/<seat>.yaml with sops isolated from the caller's identities and parses it. It is the one path from a seat to its values: `nova-secrets exec` takes it before it replaces itself with the command, and a nova tool given --seat takes it in its own process (pkg/seatcred), so no shell wrapper stands between the two and neither can check less than the other.

type SeatInjectOptions

type SeatInjectOptions struct {
	StoreDir string
	AsName   string // the existing seat receiving the values
	From     string // a seat this machine can open
	Only     string // comma-separated key names to deliver
	KeyPath  string // this machine's key: the source seat's identity
	SopsPath string
	GHPath   string
	GitPath  string

	NoPR bool

	// DryRun prints the plan and writes nothing: the source is decrypted (the read the
	// verb needs to know the names exist) but nothing is encrypted, no file is written
	// and no git write, push or gh call runs. The plan is read off the same carry the
	// real run walks, so the two cannot differ.
	DryRun bool

	// Progress receives one short line per step that can take time (nil = silent).
	// It never carries a value: step names and public facts only.
	Progress io.Writer

	Now   func() time.Time
	Check func(storeDir, asName, keyPath, sopsPath string) error

	// Exec replaces the os/exec child process, as it does for seal.
	Exec execCommand
}

SeatInjectOptions carries one `seat inject` request and the test seams around it.

type Secret

type Secret struct {
	// contains filtered or unexported fields
}

Secret carries a credential value. The zero value is "never looked up", which is distinct from "looked up and found empty".

WHY A TYPE AND NOT A string: every formatting route is closed -- String, GoString, Format (which covers %v %s %q %x %#v and every other verb), MarshalText and MarshalJSON all yield Redacted. There is deliberately no accessor that RETURNS the value: Use hands it to a callback and takes it back, so an exposure is a visible, deliberate shape rather than an interpolation.

func NewSecret

func NewSecret(v string) Secret

NewSecret wraps a value. The caller should drop its own copy immediately.

func ReadLogin

func ReadLogin(l Login) (Secret, error)

ReadLogin reads the login's secret in this process through OpenSeatFile, the one path exec takes, so it checks everything exec checks before it decrypts. The value comes back as a Secret, never a string, and goes into no environment. A login with a field missing, a seat that does not open, and a name the seat does not hold or holds empty are each a refusal naming the login and the next thing to run; none of them is ever an empty password.

func (Secret) Empty

func (s Secret) Empty() bool

Empty reports whether the value is the empty string. A present-but-empty entry is not a credential.

func (Secret) Format

func (s Secret) Format(f fmt.State, verb rune)

Format closes every fmt verb at once. String() alone is not enough: %#v goes to GoStringer, %x and %d bypass Stringer entirely. ignored: fmt.State's write has no caller to report to; the redacted word is all it would print

func (Secret) GoString

func (s Secret) GoString() string

func (Secret) Loaded

func (s Secret) Loaded() bool

Loaded reports whether a lookup actually produced a value.

func (Secret) MarshalJSON

func (s Secret) MarshalJSON() ([]byte, error)

func (Secret) MarshalText

func (s Secret) MarshalText() ([]byte, error)

func (Secret) String

func (s Secret) String() string

func (Secret) Use

func (s Secret) Use(fn func(string) error) error

Use is the ONLY route from a Secret back to a string, and it does not return one.

type SopsConfig

type SopsConfig struct {
	CreationRules []CreationRule
}

SopsConfig holds creation rules parsed from .sops.yaml.

func ParseSopsConfig

func ParseSopsConfig(storeDir string) (*SopsConfig, error)

ParseSopsConfig parses creation rules from .sops.yaml in storeDir.

type StoreFileKey

type StoreFileKey struct {
	Name  string
	Value string
	Clear bool
}

StoreFileKey represents a top-level key and clear status in a store file.

func ParseStoreFileWithoutDecrypting

func ParseStoreFileWithoutDecrypting(filePath string) (keys []StoreFileKey, recipients []string, hasSops bool, err error)

ParseStoreFileWithoutDecrypting extracts keys, clear status, and recipients from a sealed yaml.

type UnitKeyLogin

type UnitKeyLogin struct {
	Store string
	As    string
	Key   string
	Sops  string
	Names []string
}

UnitKeyLogin is a seat login that names every secret a sprint unit reads in its own process, not only the store password. Names is the setting: the decision key, each provider key. It holds no secret, so a tool may record it and print every field.

func (UnitKeyLogin) Missing

func (u UnitKeyLogin) Missing() string

Missing names every field left empty, as the flags that set them. "" when none is. A name list left empty is --keys.

func (UnitKeyLogin) String

func (u UnitKeyLogin) String() string

String is the login on one line. Every field is a fact; none is a secret.

Jump to

Keyboard shortcuts

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