keymgmt

package
v1.0.3 Latest Latest
Warning

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

Go to latest
Published: Sep 3, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

Documentation

Overview

Package keymgmt holds the safe header-write protocol shared by every operation that mutates the key-slot header. A clobbered or half-written header locks the user out of every blob under it, so writes go through SafePut: back up the current header, confirm it hasn't changed underneath us, write, then read back and verify the result still unlocks before trusting it. RestoreBackup is the recovery counterpart.

This is the one place that needs both the storage layer (backend) and the crypto layer (crypto): the backend handles backup and the raw read/write, crypto parses and unlocks the result. Keeping the orchestration here lets each of those layers stay unaware of the other.

Index

Constants

This section is empty.

Variables

View Source
var ErrEpochChanged = errors.New("the vault's master key changed since this operation started")

ErrEpochChanged reports that the vault's header no longer wraps the master key the caller is holding: a rotation (or a substitution) landed since the caller unlocked. The caller's in-flight work was sealed under a superseded key and must not be left on storage.

View Source
var ErrNarrowIncomplete = errors.New("re-keying did not finish: some blobs still carry the old master")

ErrNarrowIncomplete reports that the post-flip narrow pass did not finish: the header is already re-keyed (committed at the flip), but some recorded blobs are still encrypted to the old master as well, so a holder of the old credential can still read them until a re-run completes. For a precautionary rotate this is merely "re-run to finish"; for an offboarding (a slot was removed) it means the removed credential is NOT yet revoked, which the caller must say plainly.

View Source
var ErrNoPath = errors.New("no signed rotation path from the pinned master to the current one")

ErrNoPath reports that no chain of valid signed transitions connects the pinned master to the observed one.

Functions

func Descends added in v0.14.0

func Descends(h *crypto.Header, mk *crypto.MasterKey, accept func(signPub string) bool) error

Descends proves that the master a header now wraps descends, through valid signed transitions, from some past signing key the caller recognizes (via accept). It serves the onboarding fingerprint: the code in hand digests a signing key that may have been rotated away between `credential add` and first contact, so the verifier must accept any recognized ancestor, not only the current key. The caller checks the current key itself before calling; this only searches history. Revisions are unbounded below: the fingerprint carries no revision, and the chain's signatures are what bind it.

func FollowRotations added in v0.7.0

func FollowRotations(h *crypto.Header, pinned string, pinnedRevision int, mk *crypto.MasterKey) error

FollowRotations proves, from the header's signed transitions alone, that the master a header now wraps descends legitimately from the master this machine pinned. It searches for a chain of valid transitions from the pinned signing key to the header's, each signed by its predecessor, all within this vault, with strictly increasing revisions that stay within the pinned revision's future and end no later than the observed header's. mk is the unlocked master the header yielded; the final hop must name exactly it, binding the chain to the key actually in hand rather than to header metadata.

A nil error means the pin may advance to the observed header silently. Any failure returns ErrNoPath: the caller falls back to the alarm it would have raised anyway: walking can clear an alarm, never create one.

func RestoreBackup

func RestoreBackup(ctx context.Context, store backend.HeaderStore) error

RestoreBackup copies the header backup back over the header and confirms the result parses, so recovery from a bad write is one command instead of a raw rclone incantation. It reports a clear error when there is no backup to restore (none has been written yet: a vault gets its first backup on its second header write).

func RotateMaster

func RotateMaster(ctx context.Context, store Vault, hdr *crypto.Header, base []byte, oldMK *crypto.MasterKey, verify func(*crypto.Header) (*crypto.MasterKey, error), onFlip func(*crypto.MasterKey)) (*crypto.MasterKey, error)

RotateMaster re-keys a vault: it mints a fresh master, re-encrypts every manifest-recorded blob under it, and rewrites the header to wrap the new master under hdr's slots, with the manifest re-keyed in the same write, since the blob MACs are derived from the master. hdr carries the desired slot set (unchanged for a precautionary rotate, minus a slot for offboarding) and the manifest the caller verified; base is the current header bytes (freshness); oldMK the current master; verify re-unlocks the new header with the operator's credential. Returns the new master.

It never leaves a reader unable to decrypt, via a three-phase transition that keeps the invariant "the master the header yields is a recipient of every recorded blob" at all times:

  1. widen: re-encrypt every recorded blob to {old, new}, header still yields old. Each blob's plaintext is checked against its manifest MAC first: a reverted or substituted blob must never be laundered into the new epoch.
  2. flip: write the new header (yields new, manifest re-MAC'd under new); every recorded blob already has new.
  3. narrow: re-encrypt every recorded blob to {new} only.

Each namespace's current blob and its one-generation backup (Prev) are both re-keyed, so the backstop survives the rotation. A current blob missing from storage fails the rotation, the same alarm a read raises: dropping its entry would erase the evidence of a deletion. A missing backup is not fatal: it is never served, so its loss only narrows recovery, and the rotated entry simply carries no backup until the namespace's next write re-establishes one.

Writes racing the rotation resolve through the header compare-and-swap. A concurrent writer's header update either lands before the flip, bumping the revision, so the flip's freshness check aborts the rotation (re-run), or reaches the swap after the flip, sees the new master, and rolls its own write back.

A crash loses the in-memory new master, but the operation is re-runnable from any state: a re-run re-keys from whatever the header currently yields.

onFlip, if non-nil, is called with the new master immediately after the header flip succeeds (before the narrow pass). The caller uses it to advance its local rollback pin to the new master at the moment it becomes authoritative, so an interrupted narrow doesn't leave the pin behind the header (which would make a re-run look like a rollback).

func SafePut

func SafePut(ctx context.Context, store backend.HeaderStore, h *crypto.Header, base []byte, mk *crypto.MasterKey, verify func(*crypto.Header) (*crypto.MasterKey, error)) error

SafePut writes header h with the full safety protocol around it. It bumps h's revision, seals it (authentication tag) with mk, and writes the marshaled result.

  • base is the exact header bytes read at the start of the operation (nil if the storage had no header yet). The write goes through the store's SwapHeader, which refuses (backend.ErrHeaderChanged) if the stored header no longer matches base, so a concurrent header mutation can't be silently overwritten. How atomic that is depends on the store; callers that can re-apply their change retry on ErrHeaderChanged.
  • mk is the master the header should wrap; SafePut seals with it and, after writing, confirms the read-back header's authentication tag verifies under it (catches a corrupted/substituted write).
  • verify re-unlocks the read-back header with the caller's own credential (a passphrase or an age identity) and must yield mk, an end-to-end check that the header is still usable by the caller before they walk away.

On any verification failure SafePut returns an error and leaves the backup in place; recover with RestoreBackup.

func UpdateHeader added in v0.18.0

func UpdateHeader(ctx context.Context, store backend.HeaderStore, mk *crypto.MasterKey, mutate func(*crypto.Header) error) (*crypto.Header, error)

UpdateHeader records a header mutation under the compare-and-swap. It is the write-epoch check and the mutation in one authenticated step: every attempt re-reads the header, requires it to wrap mk (else ErrEpochChanged, so a rotation that landed since the caller unlocked surfaces before anything is written), and verifies its tag, then calls mutate to modify the parsed header in place. When another writer merely lands first (backend.ErrHeaderChanged), the loop re-reads and re-runs mutate against the fresh header, so a blob-and-pointer write re-reads the now-current blob and re-applies its change. Returns the header as written, for the caller's local rollback pin.

mutate runs once per attempt. A write path that creates an object inside mutate must clean up the object from a superseded attempt (it is handed the fresh header each time, so it writes a fresh object each time); the caller tracks and deletes the orphan. On a non-retryable error from mutate or the swap, the caller deletes whatever the last attempt created.

The header's recipient field is attacker-writable in principle (it is only authenticated by a tag keyed from the master it names), so a mismatch is treated as "redo the unlock ceremony" (ErrEpochChanged), never as a reason to trust anything new here. When the recipient does match mk, the tag must verify under it, so a tampered header cannot pass as "unchanged".

Types

type Vault

type Vault interface {
	backend.Backend
	backend.HeaderStore
}

Vault is a backend that holds both the ciphertext objects and the key header, which rotation needs together.

Jump to

Keyboard shortcuts

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