identity

package
v0.55.19 Latest Latest
Warning

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

Go to latest
Published: Nov 5, 2026 License: Apache-2.0 Imports: 55 Imported by: 0

Documentation

Overview

Package identity provisions the PocketID owner, the device credential and the Owner recovery (recovery set and recovery code) at the Authority.

PocketID v2 is passkey-only: there is no password field on user creation. The owner-activation flow is therefore three calls:

  1. POST /api/users -- create the user record (no password).
  2. PUT /api/users/:id/user-groups -- add to the "owners" group.
  3. POST /api/users/:id/one-time-access-token -- issue an enrollment token.

The token is rendered into a setup URL of the form

https://id.<domain>/lc/<token>?redirect=%2Fsettings%2Faccount

which the owner clicks once to register a WebAuthn credential. The token is single-use and consumed by PocketID on first redemption.

The local provisioner only supports Source=="local" (a daily-admin owner provisioned locally on the first node). Source=="cloud" is orchestrator managed by TechStack/kombify Cloud and must not call this local provisioner.

Index

Constants

View Source
const (
	RecoveryRequestPath  = "/api/v1/identity/recovery/request"
	RecoveryCompletePath = "/api/v1/identity/recovery/complete"
	RecoveryCancelPath   = "/api/v1/identity/recovery/cancel"
	RecoveryNoticesPath  = "/api/v1/identity/recovery/notices"
	// RecoveryDeviceNoticesPath lets an enrolled Owner device fetch the
	// notices addressed to it with a DPoP proof (GET), until push delivery
	// (Companion Feed, Hub Web Push) carries them.
	RecoveryDeviceNoticesPath = "/api/v1/identity/recovery/device-notices"

	// RecoveryDeviceDelay is how long a recovery started from an enrolled
	// Owner device waits, with notices to every Owner device, before its code
	// registers a new passkey. It is not a caller choice.
	RecoveryDeviceDelay = 24 * time.Hour
	// RecoveryRequestWindow is how long a pending device recovery stays
	// completable once it became eligible.
	RecoveryRequestWindow = 72 * time.Hour
	// RecoveryPasskeyLinkLifetime bounds the Pocket ID one-time link a
	// redeemed recovery code yields.
	// Pocket ID issues a six-character typed login code for a lifetime of
	// 15 minutes or less, which its one-time link page does not redeem, so the
	// link lives 20 minutes and carries a full token.
	RecoveryPasskeyLinkLifetime = 20 * time.Minute

	// Notice kinds; each notice lists the Owner devices it is addressed to.
	RecoveryNoticeRequested    = "recovery-requested"
	RecoveryNoticeCompleted    = "recovery-completed"
	RecoveryNoticeCancelled    = "recovery-cancelled"
	RecoveryNoticeRedeemed     = "recovery-redeemed-at-console"
	RecoveryNoticeCodeRejected = "recovery-code-rejected"
	RecoveryNoticeCodeRotated  = "recovery-code-rotated"
	RecoveryNoticeRotated      = "recovery-rotated"
	RecoveryNoticeRestored     = "recovery-restored"
)
View Source
const (
	RecoverySetSchema = "stackkit.identity-recovery-set/v1"

	// The Docker volumes of the identity runtime, by compose volume name.
	RecoveryVolumePocketID = "pocketid-data"
	RecoveryVolumeStepCA   = "step-ca-db"

	// DefaultRecoveryBundleDir holds the Authority's local copy of its latest
	// set, break-glass-<node>.age. It is safe to copy anywhere.
	DefaultRecoveryBundleDir = "/var/lib/stackkit/recovery"
)
View Source
const (
	HomeAppPassHeader   = "Stackkit-App-Pass"
	HomeAppPassLifetime = 30 * 24 * time.Hour
	// HomeAppPassesPath lists (GET) and issues (POST) the caller's passes;
	// POST HomeAppPassesPath/{id}/revoke revokes one.
	HomeAppPassesPath = "/api/v1/identity/home/app-passes"
)

App passes (homelab identity target §6.3, §12): where the Owner allowed app access away for a service (appAccessAway on its access policy), the Hub issues a device's app (Immich, a WebDAV client ...) a pass of at most 30 days for that one host. The app sends it in the HomeAppPassHeader, which the router strips before the backend; the Access Gate admits it on that host's app routes only while the pass and its device are active, so revoking the device ends its passes. The Authority keeps only a hash of each pass.

View Source
const (
	// HomeLaunchPath is the reserved path on every gated host at which the
	// Gate exchanges a launch proof for a session.
	HomeLaunchPath = "/.well-known/stackkit/launch"
	// HomeLaunchProofType is the JWS "typ" of a launch proof.
	HomeLaunchProofType = "stackkit-launch+jwt"
	// HomeLaunchProofWindow is #HomelabIdentityLifetimesV1.launchProofSeconds.
	HomeLaunchProofWindow = 60 * time.Second
	// HomeBrowserSessionCookie and HomeLaunchStateCookie are host-only
	// cookies of the gated host.
	HomeBrowserSessionCookie = "__Host-stackkit-session"
	HomeLaunchStateCookie    = "__Host-stackkit-launch"
	HomeLaunchStateLifetime  = 5 * time.Minute
	// The session lifetimes of #HomelabIdentityLifetimesV1.
	HomeBrowserSessionAway = time.Hour
	HomeBrowserSessionHome = 4 * time.Hour
)

Browser sessions (homelab identity target §6.3, §12): a browser carries no per-request proof, so the Access Gate sends it to the Hub, which signs a 60 second launch proof with the device key for exactly one host; the Gate exchanges it for a per-host "__Host-" session recorded against the device: one hour away, four hours at home, never longer than the device stays active. Renewal is the same launch, silently, wherever the Hub key is reachable (on iOS by reopening from the Hub).

View Source
const (
	HomeApprovalHome      = "home"
	HomeApprovalOwnDevice = "own-device"
	HomeApprovalOwner     = "owner"
	HomeApprovalPending   = "pending"
)

Approval modes of a registry entry (#DeviceEnrollmentPolicy): approved automatically at home, confirmed away by a step-up from the person's own enrolled device or by the Owner at the Authority, or still pending.

View Source
const (
	HomeCustodyHardware    = "hardware"
	HomeCustodyOSProtected = "os-protected"
	HomeCustodySoftware    = "software"
)

Custody classes of a device key (#DeviceCustodyClassV1, Connect D4).

View Source
const (
	// HomeDeviceCredentialType is the JWS "typ" of the device credential.
	HomeDeviceCredentialType = "stackkit-device-credential+jwt"
	// HomeDeviceProofType is the JWS "typ" of a DPoP proof.
	HomeDeviceProofType = "dpop+jwt"

	// The lifetimes of #HomelabIdentityLifetimesV1; none is a caller choice.
	HomeDeviceCredentialLifetime       = time.Hour
	HomeDeviceProofWindow              = 30 * time.Second
	HomeDeviceUserVerificationInterval = 24 * time.Hour
	HomeDeviceRegistryEntryUnused      = 180 * 24 * time.Hour
	HomeRevocationTarget               = 60 * time.Second
	// HomeUserVerificationFreshness bounds the age of the passkey assertion a
	// renewal presents, as for an Owner step-up.
	HomeUserVerificationFreshness = 60 * time.Second
	// HomeClockLeeway tolerates clock skew between device, issuer and verifier.
	HomeClockLeeway = 10 * time.Second
)

The device credential of the homelab identity target (§6.1, §12): the client generates the device key, the Authority signs a key-bound credential that lives one hour, and the client renews it with a DPoP proof (RFC 9449). Every request carries the credential and a fresh proof; the server running the service verifies both. There is no X.509 device certificate.

View Source
const (
	// HomeEnrollmentPort is the catalog listener stackkit-server-home-enroll
	// (foundation/architecture_v2_catalog.cue): TLS 1.3 on the node site
	// address, exposure "lan".
	HomeEnrollmentPort = 9443
	// HomeEnrollmentServerName is the enrollment listener's certificate name.
	HomeEnrollmentServerName = localevidence.HomeEnrollmentServerName
	// HomeEnrollmentURIScheme prefixes the enrollment bootstrap a device scans.
	HomeEnrollmentURIScheme = "stackkit-enroll"
)
View Source
const (
	HomeEnrollmentBootstrapPath = "/api/v1/identity/home/enroll/bootstrap"
	HomeDevicesPath             = "/api/v1/identity/home/devices"
	HomeNoticesPath             = "/api/v1/identity/home/notices"

	// HomeOwnerEnrollConfirmPath is the Owner's confirmation page for an
	// away enrollment on the Authority's step-up origin; it takes ?id=<device
	// ID> and is served only when the Owner is the confirmer.
	HomeOwnerEnrollConfirmPath = "/api/v1/identity/step-up/enroll-confirm"
)

Routes of the Authority's node API that a device client calls with its own credential and proof (or, to enroll, its person's passkey sign-in) instead of the node API key. The notice, confirmation and undo routes are what the Hub, the Companion and the State Console deliver to people.

View Source
const (
	HomeOverlayInterfaceName    = "wg-stackkit"
	HomeOverlayListenPort       = 51820
	HomeOverlayKeepaliveSeconds = 25
)

This is the WireGuard tool of the optional VPN add-on (#VPNAddOnV1 in foundation/network.cue, D-2026-11-02-12). A VPN is never the default and never the realization of private remote access; the Owner switches it on in addition to the standard reach paths. Its peers are exactly the enrolled devices that registered a WireGuard public key and are neither revoked nor expired. The node private key is generated here into local custody and never leaves it: the rendered interface configuration loads it from the custody file at interface start.

View Source
const (
	HomeNoticeEnrolledAtHome    = "enrolled-at-home"
	HomeNoticeEnrollmentPending = "enrollment-pending"

	HomeNoticeOpen      = "open"
	HomeNoticeUndone    = "undone"
	HomeNoticeConfirmed = "confirmed"
	HomeNoticeClosed    = "closed"

	// Confirmers of an away enrollment, in the contract's order.
	HomeConfirmerOwnDevice = "own-enrolled-device"
	HomeConfirmerOwner     = "owner"
)

Notice kinds and states (records the device clients and the State Console deliver; Web Push and the Companion Feed are later slices).

View Source
const (
	HomeArtifactAccount       = "homelab.account"
	HomeArtifactDevice        = "homelab.device"
	HomeArtifactSession       = "homelab.session"
	HomeArtifactAppCredential = "homelab.app_credential"
	HomeArtifactVPNPeer       = "homelab.vpn_peer"
)

Revocation registry artifact IDs (workspace revocation-registry.v1.json).

View Source
const (
	HomeRevokedByPerson  = "person"
	HomeRevokedByOwner   = "owner"
	HomeRevokedByUndo    = "undo"
	HomeRevokedByRemoval = "account-removed"
)

Revocation reasons recorded on a registry entry.

View Source
const (
	HomeVerifierPath      = "/api/v1/identity/home/verify"
	HomeEnrollmentPath    = "/api/v1/identity/home/enroll"
	HomeDeviceRenewalPath = "/api/v1/identity/home/device/renew"
)
View Source
const (
	// HomeVPNPeerServicePort is the peer service's TLS listener on the node
	// site address and the VPN.
	HomeVPNPeerServicePort = 9444
	// HomeVPNServerName is the first DNS name of the peer service's
	// certificate, which native clients pin with the step-ca root.
	HomeVPNServerName = localevidence.HomeVPNServerName

	HomeVPNInfoPath    = "/api/v1/vpn"
	HomeVPNPeerPath    = "/api/v1/vpn/peer"
	HomeVPNConfirmPath = "/api/v1/vpn/peer/confirm"

	// HomeVPNUnboundGrace bounds how long a join key and an unbound node of a
	// coordinated tool live before the reconciler removes them.
	HomeVPNUnboundGrace = 15 * time.Minute

	HomeVPNToolWireGuard = "wireguard"
	HomeVPNToolHeadscale = "headscale"
	HomeVPNToolTailscale = "tailscale"
	HomeVPNToolPangolin  = "pangolin"
)

The optional VPN add-on (#VPNAddOnV1 in foundation/network.cue, the tool catalog in addons/vpn, homelab identity target §6.2 and §8). It is never a default: it exists only after the Owner's opt-in at the Authority's console, which this file records. Its peers are exactly the enrolled, unrevoked, unexpired devices of the registry:

  • an active device asks the host's peer service for its peer with its device credential and a DPoP proof (IssueHomeVPNPeer); a pending away enrollment is not active and gets none;
  • a coordinated tool's node binds to the device that received its join key, either by that key (Headscale) or by the device's probe through the VPN (BindHomeVPNPeer);
  • every registry change and a periodic pass reconcile the tool's live nodes with the registry (ReconcileHomeVPNPeers): the nodes of revoked, expired or removed devices, and nodes no active device bound within the grace, are deleted. A ledger entry is forgotten only after its node and key are gone, so a failed deletion is retried, never lost.

The WireGuard tool keeps its peers in the overlay peer registry (home_overlay.go); this ledger holds the coordinated tools.

View Source
const (
	HomeVPNPeerIssued   = "issued"
	HomeVPNPeerBound    = "bound"
	HomeVPNPeerRemoving = "removing"
)

Ledger states of one device's peer.

View Source
const HomeDeviceClientID = localowner.HomeDeviceClientID

HomeDeviceClientID is the Pocket ID OIDC client through which a device client signs its person in with a passkey: enrollment, the 24 hour user verification of renewal and a person's step-up. Its ID tokens are the only accepted person assertions.

View Source
const HomeDeviceRegistryFile = homeDeviceRegistryPath

HomeDeviceRegistryFile is the device registry below the workspace root; the VPN add-on resync watches it for revocations.

View Source
const HomeEnrollmentPendingLifetime = 24 * time.Hour

HomeEnrollmentPendingLifetime bounds how long an away enrollment waits for its confirmation before it lapses.

View Source
const HomeEnrollmentUndoWindow = 24 * time.Hour

HomeEnrollmentUndoWindow is #HomelabIdentityLifetimesV1.enrollmentUndoSeconds.

View Source
const (
	// HomeHubPathPrefix is the Hub's path on the Hub origin. The device
	// sign-in client's redirect (localowner.HomeDeviceCallbackPath) is inside
	// it, so the installed app's scope covers its own sign-in.
	HomeHubPathPrefix = "/device/"
)

The Homelab Hub (homelab identity target §6.1, §6.3, D-2026-11-02-10) is the device client of everyone without the kombify app: an installable PWA on the Hub origin (https://base.<domain>) that holds a WebCrypto non-extractable device key, enrolls it with the person's own passkey, renews its device credential with DPoP proofs, lists and revokes the person's devices, confirms away enrollments and receives notices by Web Push. Its routes are a sign-in surface: the shell grants nothing, and every identity route authenticates the device proof or the passkey sign-in itself.

View Source
const HomeOverlayNodeKeyFile = homeOverlayNodePrivateKeyPath

HomeOverlayNodeKeyFile is the overlay node private key in custody (wg's base64 form), below the workspace root. Only the root resync reads it, to set the key on a running interface again after wg syncconf.

View Source
const HomeOverlayPeerRegistryFile = homeOverlayPeerRegistryPath

HomeOverlayPeerRegistryFile is the VPN add-on peer registry below the workspace root.

View Source
const (
	HomePublicTrustSchema = "stackkit.home-public-trust/v2"
)
View Source
const (
	HomePushSubscriptionPath = "/api/v1/identity/home/push/subscription"
)

Web Push delivery of the notice records (homelab identity target §6.2): the Hub subscribes its own enrolled device through its push service, and the Authority sends each new enrollment or recovery notice to the subscriptions of exactly the devices the record names, encrypted to the subscription (RFC 8291) and signed with the Authority's VAPID key (RFC 8292). A payload carries the notice kind, its id and the device name, never a credential.

View Source
const (
	HomeSignerListenerCertificatePath = "/v1/listener-certificate"
)

The host-side identity signer (homelab identity target §4, §9; StackKits enpy). It runs on the Authority's host with the Owner custody and is the only party that uses the Owner key, the step-ca root and the Pocket ID admin key for the Basement core: it keeps the Owner-signed Home trust, the public Authority record, the recovery notice projection and the household groups current, and answers exactly two requests from the core over a Unix socket that only the core server's account may use:

POST /v1/listener-certificate   a CSR for the LAN enrollment listener's own
                                names only; answer: the verified chain (PEM)
POST <recovery routes>          the Owner device recovery routes, which
                                authenticate the device proof and the code
                                themselves and mint the recovery link

It never signs data the core chooses and never returns a key.

View Source
const (
	HomeStepUpRequestsPath = "/api/v1/identity/home/step-up/requests"
)

Owner step-up requests carry a console action that changes identity custody (homelab identity target §9, §10, §12: rotating the recovery key, issuing a new recovery code) to the Owner's passkey. The console raises the request with the action digest and a fresh challenge; the Owner approves it in the Hub on an enrolled Owner device with a passkey sign-in whose nonce binds exactly that action and challenge. The Hub only delivers the ID token: the console itself verifies it (VerifyHomeStepUp, single use, at most 60 s old) before it acts, so a server that stored a token decides nothing.

View Source
const HomeVPNCustodyDir = homeVPNCustodyDir

HomeVPNCustodyDir is the add-on's custody directory below the workspace root: the selection, the peer ledger, the tools' secrets and state.

View Source
const HomeVPNSelectionFile = homeVPNSelectionPath

HomeVPNSelectionFile is the Owner's opt-in below the workspace root; the overlay resync watches it with the registries.

View Source
const StaticAPIKeyFilename = "pocketid-static-api-key"

StaticAPIKeyFilename is the basename of the on-disk file that holds the PocketID STATIC_API_KEY for a homelab. It lives under <homelab>/.stackkit/.

Variables

View Source
var (
	ErrRecoveryNotConfigured = errors.New("identity: recovery is not set up on this Authority; run `stackkit break-glass setup --owner-approve`")
	ErrRecoveryConfigured    = errors.New("identity: recovery is already set up; replace it with `stackkit break-glass setup --rotate --owner-approve`")
	// ErrRecoveryDenied is the one answer to a wrong, consumed or throttled
	// code and to a refused device proof, so a refusal reveals no reason.
	ErrRecoveryDenied  = errors.New("identity: recovery code or Owner device proof refused")
	ErrRecoveryNotYet  = errors.New("identity: a device recovery becomes eligible 24 hours after it was requested")
	ErrRecoveryPending = errors.New("identity: another Owner device recovery is pending; cancel it first")
)
View Source
var (
	// ErrRecoverySetRefused is the one answer to a set that does not open
	// with the given key, fails its MAC or digests, or is malformed.
	ErrRecoverySetRefused = errors.New("identity: the recovery set does not open with this recovery key or was altered")
	// ErrRecoveryRestoreConflict refuses a restore onto a workspace that
	// already holds another Owner or another identity origin.
	ErrRecoveryRestoreConflict = errors.New("identity: this workspace already holds another Owner or identity origin; restore onto a fresh Authority")
)
View Source
var (
	// ErrHomeRegistryFull refuses a change that would grow the registry past
	// its bound; revoking devices still works.
	ErrHomeRegistryFull   = errors.New("identity: the device registry is full; revoke unused devices first")
	ErrHomeDeviceRevoked  = errors.New("identity: this device key was revoked and cannot enroll again; generate a new key")
	ErrHomeDeviceConflict = errors.New("identity: this device key is already enrolled or pending, or another device of this account uses this name")
	ErrHomeDeviceNotFound = errors.New("identity: no enrolled device has this id")
)
View Source
var (
	// ErrHomeAccessDenied is the one answer to every refused credential,
	// proof, renewal or enrollment, so a refusal reveals no reason.
	ErrHomeAccessDenied = errors.New("identity: a current device credential and request-bound proof are required")
	// ErrHomeUserVerificationRequired means the renewal was otherwise valid
	// but the last user verification is older than 24 hours: the client signs
	// the person in with a passkey through HomeDeviceClientID and renews again
	// with the ID token and a new proof.
	ErrHomeUserVerificationRequired = errors.New("identity: renewal needs a passkey user verification")
)
View Source
var (
	ErrHomeOverlayDeviceInactive = errors.New("identity: only an enrolled, unrevoked, unexpired device can join the overlay")
	ErrHomeOverlayKeyInvalid     = errors.New("identity: overlay public key must be a base64 32-byte WireGuard key")
	ErrHomeOverlayKeyInUse       = errors.New("identity: another device already registered this overlay key")
	ErrHomeOverlayPoolExhausted  = errors.New("identity: the overlay address pool has no free address")
	// ErrHomeOverlayCustodyAbsent: this workspace holds no VPN add-on custody
	// (the core's identity workspace, or a host without the add-on).
	ErrHomeOverlayCustodyAbsent = errors.New("identity: no VPN add-on custody in this workspace")
)
View Source
var (
	ErrHomeEnrollmentRateLimited = errors.New("identity: too many enrollment attempts for this account; try again later")
	ErrHomeDeviceCapReached      = errors.New("identity: this account has reached its device limit; revoke a device first")
	ErrHomeEnrollmentPending     = errors.New("identity: this device's enrollment waits for its confirmation")
	ErrHomeNoticeNotFound        = errors.New("identity: no open notice with this id is visible to this caller")
	// ErrHomeActorInUndoWindow refuses a device enrolled at home less than
	// 24 hours ago when it acts on another device: within its own undo window
	// it may revoke only itself, so a device enrolled by someone else cannot
	// lock the person out before they undo it.
	ErrHomeActorInUndoWindow = errors.New("identity: a device enrolled at home within the last 24 hours can revoke only itself")
)
View Source
var (
	// ErrHomeStepUpDenied is the one answer to a step-up that does not prove a
	// fresh passkey authentication of the required approver for exactly this
	// action, or that was already used.
	ErrHomeStepUpDenied = errors.New("identity: a fresh passkey step-up for exactly this action is required")
	// ErrOwnerStepUpDenied is returned for any Owner step-up ID token that does
	// not prove a fresh passkey authentication of the pinned Home Owner.
	ErrOwnerStepUpDenied = errors.New("identity: a fresh Owner passkey step-up for this Home is required")
)
View Source
var (
	ErrHomeVPNOff            = errors.New("identity: the VPN add-on is off; the Owner switches it on with stackkit vpn enable")
	ErrHomeVPNDeviceInactive = errors.New("identity: only an enrolled, unrevoked, unexpired device receives a VPN peer")
	ErrHomeVPNNodeBound      = errors.New("identity: this VPN node belongs to another device")
	ErrHomeVPNNoPeer         = errors.New("identity: this device has no VPN peer of the selected tool; ask for one first")
	ErrHomeVPNLedgerFull     = errors.New("identity: the VPN peer ledger is full")
)
View Source
var ErrHomeAppPassFreshPasskey = errors.New("identity: issuing an app pass needs a passkey sign-in on this device within the last 10 minutes")
View Source
var ErrHomeEnrollmentUnavailable = errors.New("identity: this server has no applied device enrollment policy")

ErrHomeEnrollmentUnavailable means the applied plan carries no admissible #DeviceEnrollmentPolicy, so this server enrolls nothing.

View Source
var (

	// ErrHomeStepUpRequestExpired ends a console wait without an approval.
	ErrHomeStepUpRequestExpired = errors.New("identity: the Owner did not approve the step-up in time")
)

HomeVPNTools are the add-on's alternative tools (#VPNAddOnToolV1).

Functions

func AwaitHomeOwnerStepUp

func AwaitHomeOwnerStepUp(ctx context.Context, root string, request HomeOwnerStepUpRequest, poll time.Duration) (string, error)

AwaitHomeOwnerStepUp waits until the Owner delivered a step-up for request and verifies it here: the pinned Owner's passkey through the device sign-in client, for exactly this action and challenge, at most 60 seconds old and never used before. The request is removed whatever the outcome.

func BindHomePublicTrust

func BindHomePublicTrust(root string, trust HomePublicTrust) error

BindHomePublicTrust is local Owner publication of the current Pocket ID pin. It does not enroll a device or mint a credential. Every device of a revoked Homelab account is revoked in the registry before the pin is published, so the account's credentials stop renewing at once and stop verifying within the revocation refresh.

func CancelDeviceRecovery

func CancelDeviceRecovery(root string, request RecoveryDeviceRequest, now time.Time) (bool, error)

CancelDeviceRecovery ends a pending device recovery from any enrolled Owner device; no code is needed to stop a recovery.

func CancelRecoveryAtConsole

func CancelRecoveryAtConsole(root string, now time.Time) (bool, error)

CancelRecoveryAtConsole ends a pending device recovery from the console.

func ClearHomeVPNSelection

func ClearHomeVPNSelection(root string) error

ClearHomeVPNSelection switches the add-on off. The caller removes the tool's peers first.

func ConfigureHomeIdentityOrigins

func ConfigureHomeIdentityOrigins(root string, origins ...string) error

ConfigureHomeIdentityOrigins sets the origins, besides the Hub origin of the identity runtime custody, on which this server serves its identity routes (renewal, away enrollment, the person routes), such as its Owner step-up origin. A proof for any other origin is refused.

func ForgetHomeVPNPeers

func ForgetHomeVPNPeers(root string, settled []HomeVPNPeer, now time.Time) error

ForgetHomeVPNPeers settles entries after the tool deleted their node and key: an entry is dropped, except an unbound one still inside the grace, which stays as a tombstone without key (see planHomeVPNRemovals). An entry that changed meanwhile is kept as it is.

func FormatRecoveryCode

func FormatRecoveryCode(code string) string

FormatRecoveryCode prints a code in groups of four for the Owner to write down.

func HomeAccountGroups

func HomeAccountGroups(root, account string, now time.Time) ([]string, bool)

HomeAccountGroups returns the household groups of account and whether they are known: the projection is readable, at most homeAccountGroupsMaxAge old, and lists the account.

func HomeDeviceEnrollmentNonce

func HomeDeviceEnrollmentNonce(thumbprint, proofID string) string

HomeDeviceEnrollmentNonce is the OIDC nonce of the passkey sign-in that enrolls a device: base64url(SHA-256("stackkit-device-enrollment/v1" 0x00 thumbprint 0x00 jti)) of the device key and the one DPoP proof that presents it.

func HomeDeviceRenewalHandler

func HomeDeviceRenewalHandler(root string) http.Handler

HomeDeviceRenewalHandler renews a device credential on the Authority. The request carries the DPoP proof in the "DPoP" header and, when a user verification is due or offered, {"userVerification": "<Pocket ID ID token>"} as its body. The proof is the authentication; no API key applies.

func HomeDeviceUserVerificationNonce

func HomeDeviceUserVerificationNonce(thumbprint, proofID string) string

HomeDeviceUserVerificationNonce is the OIDC nonce a device client sends to Pocket ID for the passkey assertion of a renewal. It binds the assertion to the device key (its JWK thumbprint) and to the one DPoP proof (its jti) that presents it: base64url(SHA-256("stackkit-device-user-verification/v1" 0x00 thumbprint 0x00 jti)).

func HomeEnrollmentCertificateSource added in v0.53.14

func HomeEnrollmentCertificateSource(root string, signer HomeAuthoritySigner) func(*tls.ClientHelloInfo) (*tls.Certificate, error)

HomeEnrollmentCertificateSource obtains the listener certificate on first use and renews it an hour before expiry. The key is generated here and never leaves the process; the host-side signer, which alone holds the Owner custody, has step-ca sign the request for exactly the listener's names. The chain is checked against the Authority's step-ca root before it is served. Without the signer the TLS handshake fails, so the listener stays closed.

func HomeEnrollmentHandler

func HomeEnrollmentHandler(root string) http.Handler

HomeEnrollmentHandler is the enrollment route of every other listener (the node API behind the reach paths). A submission there is always away and waits for its confirmation, whatever address, header or name it carries.

func HomeLaunchURL

func HomeLaunchURL(root, target, state string) (string, error)

HomeLaunchURL is the Hub page a browser without a session is sent to: the Hub signs the launch for target and state there.

func HomeVPNBoundAddresses

func HomeVPNBoundAddresses(root, tool string, now time.Time) ([]string, error)

HomeVPNBoundAddresses are the VPN addresses of the bound peers of active devices: the only sources the interface guard of a coordinated tool lets past the peer service.

func HomeVPNDeviceActive

func HomeVPNDeviceActive(root, deviceID string, now time.Time) (bool, error)

HomeVPNDeviceActive reports whether a device is an active registry entry.

func HomeVerifierHandler

func HomeVerifierHandler(root string) http.Handler

HomeVerifierHandler is the verification endpoint for a component that fronts a route in another process. The caller states the request it observed, with the route origin from its own route table. The endpoint sits behind the node API boundary; it treats no API key, workload TLS or forwarded header as identity.

func IsHomeIdentityDeviceRoute

func IsHomeIdentityDeviceRoute(r *http.Request) bool

IsHomeIdentityDeviceRoute reports the identity routes that authenticate a device proof or a person's passkey sign-in themselves (or serve only the public enrollment bootstrap), so the node API key does not apply to them.

func KeepHomePublicTrustCurrent

func KeepHomePublicTrustCurrent(ctx context.Context, root string)

KeepHomePublicTrustCurrent refreshes the Home trust at once and then every hour on a server that holds Owner custody, retrying a failed refresh every minute, until ctx ends. A server without Owner custody (yet) is checked again every homeTrustIdleInterval without a warning. While a refresh fails, the last trust stays valid to its own expiry; after that every passkey verification, enrollment and step-up fails closed.

func NewHomeEnrollmentServer added in v0.53.14

func NewHomeEnrollmentServer(root, listen string, certificate func(*tls.ClientHelloInfo) (*tls.Certificate, error)) (*http.Server, error)

NewHomeEnrollmentServer is the Authority's dedicated LAN enrollment listener, the only attestation of home: a submission it accepts directly from a peer on the site network (see homeSiteNetwork) is enrolled at once. It must run in the host's network namespace on the node site address. It serves only POST HomeEnrollmentPath over TLS 1.3 with a step-ca issued server certificate, binds every proof to the enrollment URL of the node's own custody, never the client's Host, and reads no forwarded header.

func NewHomeHubServer

func NewHomeHubServer(ctx context.Context, root, listen string, options HomeHubOptions) (*http.Server, error)

NewHomeHubServer is the Hub listener of the Authority: the Hub PWA and the device-facing identity routes, behind the core routers on the Hub host. It carries no node API key, MCP or lifecycle route.

func ObservedLANAddress added in v0.53.13

func ObservedLANAddress(address string) bool

ObservedLANAddress classifies a transport peer address the server observed (a connection RemoteAddr): private (RFC 1918 / ULA) or link-local peers only, never loopback, never read from a request header or body. It narrows who may use the LAN enrollment listener; it never establishes home on its own, which only the listener the connection arrived on does.

func ParseHomeDeviceAuthorization

func ParseHomeDeviceAuthorization(header http.Header) (credential, proof string, err error)

ParseHomeDeviceAuthorization reads the device credential from exactly one "Authorization: DPoP <credential>" header and the proof from exactly one "DPoP" header. Any other shape is refused, never guessed.

func PendingHomeEnrollment

func PendingHomeEnrollment(root, deviceID string, now time.Time) (HomeDevice, HomeStepUpAction, error)

PendingHomeEnrollment returns a pending entry and its enroll-confirm action for the Owner's confirmation page, only while the Owner is its confirmer.

func ReadHomeVPNSecret

func ReadHomeVPNSecret(root, name string) ([]byte, error)

ReadHomeVPNSecret reads one secret of the selected tool from custody.

func ReadStaticAPIKey

func ReadStaticAPIKey(baseDir string) (string, error)

ReadStaticAPIKey returns the existing STATIC_API_KEY for a homelab without generating one when the file is missing. This supports reading immutable v0.6 artifacts during migration; current native generation uses owner custody.

func RecordHomeSiteNetwork

func RecordHomeSiteNetwork(root string, now time.Time) error

RecordHomeSiteNetwork records the host's site network for the LAN enrollment listener of the Basement core. It runs on the host (Apply), where the site interface is visible, and fails when the node site address is on no interface of the host.

func RecoveryBundleName

func RecoveryBundleName(nodeRef string) string

RecoveryBundleName is the file name of a node's set.

func RecoveryDeviceHandler

func RecoveryDeviceHandler(root string, issue RecoveryPasskeyIssuer) http.Handler

RecoveryDeviceHandler serves the device recovery routes. The DPoP proof in the "DPoP" header is the authentication (no node API key); the body is {"code": "..."} for request and complete, empty for cancel, and the notices route is a GET.

func RecoveryNoticesHandler

func RecoveryNoticesHandler(root string) http.Handler

RecoveryNoticesHandler lists the recovery notices behind the node API key, for the State Console and the notice delivery of igti.6.

func RecoveryRecipient

func RecoveryRecipient(root string) (*age.HybridRecipient, error)

RecoveryRecipient is the public recipient every recovery set is encrypted to. The Authority holds nothing that decrypts a set.

func RefreshHomeAccountGroups

func RefreshHomeAccountGroups(ctx context.Context, root string, now time.Time) error

RefreshHomeAccountGroups reads the groups of every account that holds a device entry from the Authority's Pocket ID (host only: it needs the admin key) and writes the projection. A removed or disabled account has none.

func RegisterHomeHubRoutes

func RegisterHomeHubRoutes(mux *http.ServeMux, root string, options HomeHubOptions)

RegisterHomeHubRoutes mounts the Hub shell, its configuration and code exchange, and every device-facing identity route on mux.

func RegisterHomeIdentityRoutes

func RegisterHomeIdentityRoutes(mux *http.ServeMux, root string)

RegisterHomeIdentityRoutes mounts the device-facing identity routes:

GET  HomeEnrollmentBootstrapPath                     the LAN enrollment bootstrap (no secret)
GET  HomeDevicesPath                                 the caller's devices; the Owner's devices see all
POST HomeDevicesPath/{id}/revoke                     revoke own device (Owner: any) through the chain
GET  HomeNoticesPath                                 enrollment notices of the caller's account (Owner: all)
POST HomeNoticesPath/{id}/undo                       undo an at-home enrollment or decline a pending one
POST /api/v1/identity/home/enrollments/{id}/confirm  confirm a pending enrollment from an own device:
                                                     {"challenge", "stepUp"} with the person's passkey step-up

Every route but the bootstrap needs "Authorization: DPoP <credential>" and a DPoP proof for exactly its method and path.

func RenderHomeOverlayConfig added in v0.53.13

func RenderHomeOverlayConfig(root string, now time.Time) ([]byte, error)

RenderHomeOverlayConfig renders the node's wg-quick interface configuration. It carries public keys and addresses only: the private key is loaded from local custody by PostUp. Peers of revoked or expired devices are omitted, so the next render and interface reload removes their access.

func ResetRecoveryAfterRestore

func ResetRecoveryAfterRestore(root string, now time.Time) error

ResetRecoveryAfterRestore runs when a set's custody was installed on a replacement Authority. The set may predate a cancellation, a new code or a device revocation, so it must not revive a pending device recovery, the failure count, or a code the Owner may since have replaced: the pending recovery ends and the restored code counts as used until the Owner issues a new one (stackkit break-glass code).

func RevokeHomeAppPass

func RevokeHomeAppPass(root, id string, actor HomeActor, now time.Time) error

RevokeHomeAppPass revokes one pass of the actor's account (Owner: any).

func RotateRecoveryCode

func RotateRecoveryCode(root string, now time.Time) (string, error)

RotateRecoveryCode replaces the recovery code, for example after it was consumed, and returns the new code for a single display. A pending device recovery ends, because it was bound to the old code.

func SaveHomeVPNSelection

func SaveHomeVPNSelection(root string, selection HomeVPNSelection) error

SaveHomeVPNSelection records the Owner's opt-in after checking it.

func ServeHomeAuthoritySigner

func ServeHomeAuthoritySigner(ctx context.Context, root, socketPath string, peerUID int) error

ServeHomeAuthoritySigner runs the signer until ctx ends: the background duties and the socket. peerUID is the only account that may call it (the Basement core server's); root is the Authority's workspace with the Owner custody.

func SyncHomeVPNAnnouncement

func SyncHomeVPNAnnouncement(root string) error

SyncHomeVPNAnnouncement projects the selected tool into the verification custody, or removes the projection while the add-on is off (host only).

func SyncRecoveryNoticeProjection

func SyncRecoveryNoticeProjection(root string) error

SyncRecoveryNoticeProjection rewrites the notice projection from the recovery state (host only); without a state there is nothing to project.

func ValidHomeDeviceName

func ValidHomeDeviceName(value string) bool

ValidHomeDeviceName bounds the name a person gives a device: a lowercase DNS-label-like name, never a URL or principal.

func ValidateHomeVPNEndpoint

func ValidateHomeVPNEndpoint(endpoint string) error

ValidateHomeVPNEndpoint admits an Owner-declared host:port: a DNS name or an IP address and a port, nothing else.

func ValidateHomeVPNSelection

func ValidateHomeVPNSelection(selection HomeVPNSelection) error

ValidateHomeVPNSelection checks an opt-in before anything is recorded.

func VerifyHomeStepUp

func VerifyHomeStepUp(root string, action HomeStepUpAction, approver HomeStepUpApprover, challenge, idToken string, now time.Time) (string, error)

VerifyHomeStepUp accepts a step-up only when the ID token proves a passkey authentication (amr "phr") of the required approver, at most HomeStepUpWindow old, for exactly this action and challenge, against the current Owner-signed Home trust; it then records the step-up as used, so the same token never approves twice. It returns the approver's subject. While the Home trust is not current it fails closed.

func VerifyOwnerStepUp added in v0.54.8

func VerifyOwnerStepUp(root, idToken, nonce string, now time.Time) (string, error)

VerifyOwnerStepUp verifies an ID token of the local Owner step-up client for an Owner decision that is not bound to a homelab identity action, such as answering a StackKits MCP agent's write approval request. It checks the active bound Home trust, Owner custody of this Home, the pinned Owner subject, signing keys, audience, the flow nonce, a passkey authentication, revocation and HomeOwnerStepUpFreshness, and returns the Owner subject. It changes no state.

func WriteHomeVPNSecret

func WriteHomeVPNSecret(root, name string, raw []byte) error

WriteHomeVPNSecret keeps one secret of the selected tool (an OAuth client, an API key, Newt's secret) in the add-on's custody, 0600.

Types

type HomeActor

type HomeActor struct {
	Account string
	Device  string
	Owner   bool
}

HomeActor is who acts on registry entries: a device of a Homelab account (verified by its credential and proof), or the Owner, who is either a device of the Owner's account or the local CLI at the Authority.

type HomeAppPass

type HomeAppPass struct {
	ID        string     `json:"id"`
	Host      string     `json:"host"`
	Name      string     `json:"name"`
	Account   string     `json:"account"`
	Device    string     `json:"device"`
	CreatedAt time.Time  `json:"createdAt"`
	ExpiresAt time.Time  `json:"expiresAt"`
	RevokedAt *time.Time `json:"revokedAt,omitempty"`
}

HomeAppPass is one pass, without its secret.

func IssueHomeAppPass

func IssueHomeAppPass(root string, caller HomeAuthentication, host, name string, appHosts []string, now time.Time) (string, HomeAppPass, error)

IssueHomeAppPass issues a pass for host to the calling device, whose last passkey user verification must be at most 10 minutes old. host must be one the Owner opened to app passes. The secret is returned once.

func ListHomeAppPasses

func ListHomeAppPasses(root string, actor HomeActor) ([]HomeAppPass, error)

ListHomeAppPasses returns the actor's passes (the Owner's devices: all).

type HomeAuthentication

type HomeAuthentication struct {
	Subject        string    `json:"subject"`
	Device         string    `json:"device"`
	CredentialID   string    `json:"credentialId"`
	ExpiresAt      time.Time `json:"expiresAt"`
	UserVerifiedAt time.Time `json:"userVerifiedAt"`
}

HomeAuthentication is request-scoped admission of a Homelab account on an enrolled device, not privileged approval. Subject is the account (Pocket ID subject); Device is the device ID (key thumbprint).

func AuthenticateHomeVPNRequest

func AuthenticateHomeVPNRequest(root string, r *http.Request, origins []string) (HomeAuthentication, error)

AuthenticateHomeVPNRequest admits a peer service request only with the current credential of an active device and a fresh, unused DPoP proof for exactly this method, path and one of the service's own origins (from custody, never the request's Host).

type HomeAuthoritySigner

type HomeAuthoritySigner interface {
	// IssueListenerCertificate has step-ca sign a certificate request for the
	// LAN enrollment listener and returns the verified chain (PEM).
	IssueListenerCertificate(ctx context.Context, csrDER []byte) ([]byte, error)
	// RecoveryHandler serves the Owner device recovery routes at the signer.
	RecoveryHandler() http.Handler
}

HomeAuthoritySigner is the core's view of the host-side signer.

func NewHomeAuthoritySignerClient

func NewHomeAuthoritySignerClient(socketPath string) HomeAuthoritySigner

NewHomeAuthoritySignerClient is the core's client of the host-side signer.

type HomeBrowserSession

type HomeBrowserSession struct {
	Account   string
	Device    string
	Host      string
	Home      bool
	IssuedAt  time.Time
	ExpiresAt time.Time
}

HomeBrowserSession is a verified session of one device on one host.

func IssueHomeBrowserSession

func IssueHomeBrowserSession(root string, caller HomeAuthentication, host string, home bool, now time.Time) (string, HomeBrowserSession, error)

IssueHomeBrowserSession mints the session value for a launch the Gate admitted: home when the launch came through the LAN listener.

type HomeConfirmation

type HomeConfirmation struct {
	JWKThumbprint string `json:"jkt"`
}

HomeConfirmation is the key binding of a device credential (RFC 7800).

type HomeDevice added in v0.53.13

type HomeDevice struct {
	ID                 string             `json:"id"`
	Name               string             `json:"name"`
	AccountSubject     string             `json:"accountSubject"`
	Custody            string             `json:"custody"`
	PublicKey          jose.JSONWebKey    `json:"publicKey"`
	EnrolledAt         time.Time          `json:"enrolledAt"`
	LastUsedAt         time.Time          `json:"lastUsedAt"`
	LastUserVerifiedAt time.Time          `json:"lastUserVerifiedAt"`
	Approval           HomeDeviceApproval `json:"approval"`
	Revoked            bool               `json:"revoked"`
	RevokedAt          *time.Time         `json:"revokedAt,omitempty"`
	RevokedBy          string             `json:"revokedBy,omitempty"`
}

HomeDevice is the registry entry of one device key at the Authority. ID is the RFC 7638 thumbprint of the device-generated public key; the entry binds to one Homelab account (the Pocket ID subject of the person who enrolled it) and expires 180 days after its last use. It holds no private key or credential.

func ConfirmHomeEnrollmentByOwner

func ConfirmHomeEnrollmentByOwner(root, deviceID, challenge, idToken string, now time.Time) (HomeDevice, error)

ConfirmHomeEnrollmentByOwner confirms a pending enrollment with the Owner's passkey step-up at the Authority (the Owner step-up client) for the entry's enroll-confirm action and challenge. The Owner confirms only for a person who has no enrolled device of their own; otherwise that device confirms.

func ListHomeDevices added in v0.53.13

func ListHomeDevices(root string) ([]HomeDevice, error)

ListHomeDevices returns every registry entry, newest first.

func (HomeDevice) Active

func (d HomeDevice) Active(now time.Time) bool

Active reports whether the entry is approved, not revoked and not lapsed.

func (HomeDevice) ExpiresAt

func (d HomeDevice) ExpiresAt() time.Time

ExpiresAt is when the entry lapses without another use.

func (HomeDevice) Pending

func (d HomeDevice) Pending(now time.Time) bool

Pending reports whether the entry is an away enrollment awaiting its confirmation.

type HomeDeviceApproval

type HomeDeviceApproval struct {
	Mode         string     `json:"mode"`
	By           string     `json:"by,omitempty"`
	At           *time.Time `json:"at,omitempty"`
	PendingUntil *time.Time `json:"pendingUntil,omitempty"`
}

HomeDeviceApproval records how an entry was admitted. A pending entry (an away enrollment) has PendingUntil and no credential until it is confirmed; By is the confirming device ID or the Owner subject.

type HomeDeviceCredential

type HomeDeviceCredential struct {
	Credential            string     `json:"credential"`
	ExpiresAt             time.Time  `json:"expiresAt"`
	UserVerificationDueAt time.Time  `json:"userVerificationDueAt"`
	Device                HomeDevice `json:"device"`
}

HomeDeviceCredential is what enrollment and renewal return to the device. Credential is a bearer-shaped token bound to the device key: it must never be logged, and it is useless without a proof signed by that key.

type HomeDeviceRequest

type HomeDeviceRequest struct {
	Method      string `json:"method"`
	RouteOrigin string `json:"routeOrigin"`
	Path        string `json:"path"`
	Credential  string `json:"credential"`
	Proof       string `json:"proof"`
}

HomeDeviceRequest is one request to a service, as the server running the service observed it. RouteOrigin is the https origin of the matched route taken from the server's own route table, never from a request header; Path is the request target in origin form (a query is allowed and is not part of the proof). Credential is the token of "Authorization: DPoP <credential>", Proof the "DPoP" header (see ParseHomeDeviceAuthorization).

type HomeEnrollmentBootstrap added in v0.53.14

type HomeEnrollmentBootstrap struct {
	EnrollmentURL    string `json:"enrollmentUrl"`
	HubEnrollmentURL string `json:"hubEnrollmentUrl,omitempty"`
	TLSServerName    string `json:"tlsServerName"`
	CARootSHA256     string `json:"caRootSha256"`
	URI              string `json:"uri"`
	// VPN is the VPN add-on's peer service while the Owner has it on.
	VPN *HomeVPNAnnouncement `json:"vpn,omitempty"`
}

HomeEnrollmentBootstrap is what a device client needs to reach the Authority's LAN enrollment listener directly: its URL on the node site address, the server name of its certificate and the step-ca root fingerprint to pin. It contains no secret and grants nothing; the person's own passkey sign-in is the enrollment's credential.

HubEnrollmentURL is the same listener under the Hub host, which its certificate also names: the installed Hub PWA, a browser that cannot pin a root or set a server name, opens it from the home network where the Hub host resolves to the node site address. It is empty without an identity runtime address.

func LoadHomeEnrollmentBootstrap added in v0.53.14

func LoadHomeEnrollmentBootstrap(root string) (HomeEnrollmentBootstrap, error)

LoadHomeEnrollmentBootstrap derives the bootstrap from the Authority's public record: the node site address the LAN resolver custody binds and the established step-ca root. It fails closed when either is missing or the address is not a LAN address.

type HomeEnrollmentNotice

type HomeEnrollmentNotice struct {
	ID             string            `json:"id"`
	Kind           string            `json:"kind"`
	AccountSubject string            `json:"accountSubject"`
	DeviceID       string            `json:"deviceId"`
	DeviceName     string            `json:"deviceName"`
	Custody        string            `json:"custody"`
	CreatedAt      time.Time         `json:"createdAt"`
	ActionUntil    time.Time         `json:"actionUntil"`
	NotifyDevices  []string          `json:"notifyDevices"`
	NotifyOwner    bool              `json:"notifyOwner"`
	Confirmer      string            `json:"confirmer,omitempty"`
	StepUp         *HomeStepUpAction `json:"stepUp,omitempty"`
	State          string            `json:"state"`
	ResolvedAt     *time.Time        `json:"resolvedAt,omitempty"`
	ResolvedBy     string            `json:"resolvedBy,omitempty"`
}

HomeEnrollmentNotice tells the person's other devices and the Owner about one enrollment. An at-home notice offers the undo until ActionUntil; a pending notice asks for the confirmation until ActionUntil, by the Confirmer (computed when it is listed). StepUp is the action a confirmation step-up approves. NotifyDevices are the person's devices that were enrolled when the notice was created.

func ListHomeEnrollmentNotices

func ListHomeEnrollmentNotices(root string, actor HomeActor, now time.Time) ([]HomeEnrollmentNotice, error)

ListHomeEnrollmentNotices returns the notices visible to actor, newest first: those of the actor's own account and, for the Owner, every notice. A notice is open only while its action window lasts and its device is still in the state the notice is about; Confirmer reflects the current registry.

type HomeEnrollmentOutcome

type HomeEnrollmentOutcome struct {
	Status     string                `json:"status"`
	Device     HomeDevice            `json:"device"`
	Credential *HomeDeviceCredential `json:"credential,omitempty"`
	Notice     HomeEnrollmentNotice  `json:"notice"`
	// VPN announces the VPN add-on's peer service to a device that received
	// its credential, so it asks for its peer in the same enrollment.
	VPN *HomeVPNAnnouncement `json:"vpn,omitempty"`
}

HomeEnrollmentOutcome is the answer to a submission: at home the active entry and its first credential, away the pending entry. Notice is the record the person's other devices and the Owner receive.

type HomeEnrollmentPolicy

type HomeEnrollmentPolicy struct {
	MaxDevicesPerAccount int
	RateLimitAttempts    int
	RateLimitWindow      time.Duration
	CustodyClasses       []string
}

HomeEnrollmentPolicy is the runtime form of the applied plan's #DeviceEnrollmentPolicy (identity.deviceEnrollment): the per-account device cap, the per-account rate limit of enrollment submissions and the admitted device key custody classes. Every other field of the contract is fixed and checked, never configurable here.

func LoadHomeEnrollmentPolicy

func LoadHomeEnrollmentPolicy(root string) (HomeEnrollmentPolicy, error)

LoadHomeEnrollmentPolicy reads identity.deviceEnrollment from the applied plan and admits it only when it is exactly the person enrollment of the homelab identity target within the contract's bounds; anything else fails closed with ErrHomeEnrollmentUnavailable.

type HomeEnrollmentSubmission

type HomeEnrollmentSubmission struct {
	Name    string
	Custody string
	Proof   string
	Person  string
	Home    bool
	Targets []string
}

HomeEnrollmentSubmission is one enrollment as the server observed it. Proof is a DPoP proof (htm POST) by the device-generated key; the key's thumbprint becomes the device ID. Person is a Pocket ID ID token of the enrolling person from a passkey sign-in through HomeDeviceClientID with the nonce HomeDeviceEnrollmentNonce(thumbprint, proof jti). Home is set only by the LAN enrollment listener, for a connection it accepted directly; Targets are then that listener's enrollment URLs from the server's own custody (the site address and the Hub host on the listener port), one of which the proof must bind.

type HomeHubOptions

type HomeHubOptions struct {
	// Signer is the host-side identity signer. Without it the recovery routes
	// answer recovery_unavailable.
	Signer HomeAuthoritySigner
	// AppHosts are the hosts with a route the Owner opened to app passes away
	// (the Access Gate's table); the Hub issues app passes for these only.
	AppHosts []string
	// LaunchHosts are the hosts a browser session opens (user routes of the
	// Access Gate's table); the Hub signs launch proofs for these only.
	LaunchHosts []string
}

HomeHubOptions wire the Hub to what its process does not hold itself.

type HomeLaunch

type HomeLaunch struct {
	Host       string
	Credential string
	Proof      string
	State      string
	Target     string
}

HomeLaunch is one launch as the Gate observed it: the gated host the browser came to, the credential and proof the Hub put into the launch URL, and the state of the Gate's own launch cookie on that host.

type HomeOverlayNode added in v0.53.13

type HomeOverlayNode struct {
	InterfaceName       string `json:"interfaceName"`
	PublicKey           string `json:"publicKey"`
	Address             string `json:"address"`
	ListenPort          int    `json:"listenPort"`
	KeepaliveSeconds    int    `json:"persistentKeepaliveSeconds"`
	PrivateKeyInCustody bool   `json:"privateKeyInLocalCustody"`
}

HomeOverlayNode is what a device needs to configure its side of the tunnel. The endpoint stays owner-declared and is not part of this record.

func EnsureHomeOverlayNodeKey added in v0.53.13

func EnsureHomeOverlayNodeKey(root string) (HomeOverlayNode, error)

EnsureHomeOverlayNodeKey generates the node's WireGuard private key into local custody on first use and returns the node's public overlay identity. The private key is never returned.

type HomeOverlayPeer added in v0.53.13

type HomeOverlayPeer struct {
	DeviceID  string `json:"deviceId"`
	PublicKey string `json:"publicKey"`
	Address   string `json:"address"`
}

HomeOverlayPeer is the public peer record of one enrolled device. It holds only the device's WireGuard public key and its overlay address.

func BindHomeOverlayPeer added in v0.53.13

func BindHomeOverlayPeer(root, deviceID, publicKey string, now time.Time) (HomeOverlayPeer, error)

BindHomeOverlayPeer registers the device-generated WireGuard public key of an active enrolled device and assigns it one overlay address. Rebinding the same device replaces its key and keeps its address. The device is checked under the peer registry lock, which every revocation also takes after it revoked the entry, so a peer of a revoked device never survives.

type HomeOwnerStepUpRequest

type HomeOwnerStepUpRequest struct {
	ID        string           `json:"id"`
	Summary   string           `json:"summary"`
	Action    HomeStepUpAction `json:"stepUp"`
	Challenge string           `json:"challenge"`
	CreatedAt time.Time        `json:"createdAt"`
	ExpiresAt time.Time        `json:"expiresAt"`
	IDToken   string           `json:"idToken,omitempty"`
}

HomeOwnerStepUpRequest is one pending console action awaiting the Owner's passkey step-up. Summary is the text the Hub shows the Owner.

func RequestHomeOwnerStepUp

func RequestHomeOwnerStepUp(root string, action HomeStepUpAction, summary string, now time.Time) (HomeOwnerStepUpRequest, error)

RequestHomeOwnerStepUp records a console action for the Owner's approval.

type HomePocketIDTrust

type HomePocketIDTrust struct {
	Issuer   string             `json:"issuer"`
	ClientID string             `json:"clientId"`
	Subject  string             `json:"subject"`
	Keys     jose.JSONWebKeySet `json:"keys"`
}

HomePocketIDTrust is the live PocketID OIDC authority. The CUE human issuer URN cannot alias this HTTPS issuer or its OIDC client audience. ClientID is always HomeDeviceClientID, the device sign-in client the pin was bound with.

type HomePublicTrust

type HomePublicTrust struct {
	Schema          string                                  `json:"schema"`
	HomeSiteRef     string                                  `json:"homeSiteRef"`
	PocketID        HomePocketIDTrust                       `json:"pocketId"`
	RevokedSubjects []string                                `json:"revokedSubjects"`
	IssuedAt        time.Time                               `json:"issuedAt"`
	ExpiresAt       time.Time                               `json:"expiresAt"`
	Signature       localevidence.OwnerPolicyStateSignature `json:"signature"`
}

HomePublicTrust is the Owner-signed Pocket ID pin of this Home: issuer, signing keys, the Owner subject and the revoked Homelab accounts. It backs every passkey verification (enrollment, renewal user verification, every step-up). RefreshHomePublicTrust binds it in production. Device credentials are verified against the device issuer and registry, not against this pin.

func RefreshHomePublicTrust

func RefreshHomePublicTrust(ctx context.Context, root string, now time.Time) (HomePublicTrust, error)

RefreshHomePublicTrust is the production path of the Owner-signed Home trust. It registers the device sign-in client in the local Pocket ID on first use (or verifies the registered one), reads the live issuer, signing keys and Owner subject, and binds them as the Home trust for 24 hours. The new trust carries every account the previous trust revoked and every account the removal chain recorded, so a refresh never readmits one.

type HomeRenewalRequest

type HomeRenewalRequest struct {
	Method           string
	Proof            string
	UserVerification string
}

HomeRenewalRequest is one credential renewal at the Authority. Proof is a DPoP proof (htm POST, htu https://<host>HomeDeviceRenewalPath) signed by the enrolled device key. UserVerification is optional until the last user verification is 24 hours old: then it must be a Pocket ID ID token of the device's account from a passkey sign-in through HomeDeviceClientID with the nonce HomeDeviceUserVerificationNonce(thumbprint, proof jti).

type HomeRevocation

type HomeRevocation struct {
	Devices   []HomeDevice          `json:"devices"`
	Artifacts []HomeRevokedArtifact `json:"artifacts"`
}

HomeRevocation is the outcome of one revocation: the revoked entries and one line per revocation-registry artifact.

func RevokeHomeAccount

func RevokeHomeAccount(root, account string, now time.Time) (HomeRevocation, error)

RevokeHomeAccount is the local part of removing a member: every device of the account (active, pending or lapsed) is revoked with all it holds, and the account joins the revoked accounts of the Owner-signed Home trust, so no passkey assertion of it is accepted again. The Owner's account is refused. Deleting the Pocket ID user is the caller's next step.

func RevokeHomeDevice added in v0.53.13

func RevokeHomeDevice(root, deviceID string, actor HomeActor, now time.Time) (HomeRevocation, error)

RevokeHomeDevice permanently revokes one device key for actor: a person revokes only their own account's devices, the Owner any device. A device of another account is reported as not found. Revocation only removes access, so it needs no fresh step-up; the key can never enroll again.

func UndoHomeEnrollment

func UndoHomeEnrollment(root, noticeID string, actor HomeActor, now time.Time) (HomeRevocation, error)

UndoHomeEnrollment undoes an enrollment within its notice's action window: it revokes the enrolled device (an at-home enrollment within its 24 hour undo, or a pending one it declines) through the revocation chain. The person's devices and the Owner may undo; nobody else sees the notice.

type HomeRevokedArtifact

type HomeRevokedArtifact struct {
	ID     string `json:"id"`
	Status string `json:"status"`
	Detail string `json:"detail"`
}

HomeRevokedArtifact is what the chain did for one artifact. Status is "revoked", "removed", "none-issued" (no code issues this artifact yet) or "not-current" (the step could not run and the detail says what remains).

type HomeStepUpAction

type HomeStepUpAction struct {
	Class  HomeStepUpClass `json:"class"`
	Digest string          `json:"digest"`
}

HomeStepUpAction is the one action a step-up approves: its class and the SHA-256 digest (lowercase hex) of the action's canonical JSON.

func NewHomeStepUpAction

func NewHomeStepUpAction(class HomeStepUpClass, action any) (HomeStepUpAction, error)

NewHomeStepUpAction derives the action digest from the class and the action's JSON encoding. The caller passes the exact object it will act on, so a step-up for one action can never approve another.

func (HomeStepUpAction) Nonce

func (a HomeStepUpAction) Nonce(challenge string) string

Nonce is the OIDC nonce of the passkey sign-in that approves this action: base64url(SHA-256("stackkit.homelab-step-up/v1" 0x00 class 0x00 digest 0x00 challenge)). The challenge is a fresh random value (32 to 128 base64url characters) of the approving flow, so a failed attempt can be retried with a new sign-in; every nonce is accepted once.

type HomeStepUpApprover

type HomeStepUpApprover struct {
	Owner   bool
	Account string
}

HomeStepUpApprover is who must approve an action: the pinned Owner through the Owner step-up client at the Authority, or one person (Account) through the device sign-in client on their own enrolled device.

type HomeStepUpClass

type HomeStepUpClass string

Step-up classes of the homelab identity target (§6.2, §10, §12). An act on homelab identity is a passkey step-up valid for 60 seconds and bound to the digest of the one action it approves; each step-up is accepted once.

const (
	// HomeStepUpEnrollConfirm confirms an away enrollment.
	HomeStepUpEnrollConfirm HomeStepUpClass = "enroll-confirm"
	// HomeStepUpJoin approves a further server joining the Authority (P2).
	HomeStepUpJoin HomeStepUpClass = "join"
	// HomeStepUpClientRegistration approves an OIDC client registration at
	// the Authority's Pocket ID for another server or an app.
	HomeStepUpClientRegistration HomeStepUpClass = "client-registration"
	// HomeStepUpCustodyChange approves a change of identity custody (recovery
	// set, recovery code, Owner passkey reset).
	HomeStepUpCustodyChange HomeStepUpClass = "custody-change"

	// HomeStepUpWindow is #HomelabIdentityLifetimesV1.ownerStepUpSeconds: the
	// passkey authentication must be at most this old when it is presented.
	HomeStepUpWindow = 60 * time.Second
	// HomeOwnerStepUpFreshness bounds every Owner passkey step-up.
	HomeOwnerStepUpFreshness = HomeStepUpWindow
)

type HomeVPNAnnouncement

type HomeVPNAnnouncement struct {
	Tool          string `json:"tool"`
	PeerService   string `json:"peerService"`
	TLSServerName string `json:"tlsServerName"`
}

HomeVPNAnnouncement is where an enrolled device asks for its VPN peer.

type HomeVPNLiveNode

type HomeVPNLiveNode struct {
	Ref       string    `json:"ref"`
	KeyRef    string    `json:"keyRef,omitempty"`
	Addresses []string  `json:"addresses,omitempty"`
	CreatedAt time.Time `json:"createdAt"`
}

HomeVPNLiveNode is one node the tool's coordination holds under the add-on's device tag (or, for Pangolin, one of the add-on's clients).

type HomeVPNPangolinSelection

type HomeVPNPangolinSelection struct {
	Endpoint   string `json:"endpoint"`
	API        string `json:"api"`
	OrgID      string `json:"orgId"`
	SiteID     int    `json:"siteId"`
	NewtID     string `json:"newtId"`
	ResourceID int    `json:"resourceId,omitempty"`
}

HomeVPNPangolinSelection names the Owner's Pangolin server (the origin Newt and the devices' Olm connect to), its Integration API, the organization, the Newt site of this Authority and the site address resource the add-on created there.

type HomeVPNPeer

type HomeVPNPeer struct {
	DeviceID  string     `json:"deviceId"`
	Tool      string     `json:"tool"`
	State     string     `json:"state"`
	KeyRef    string     `json:"keyRef,omitempty"`
	NodeRef   string     `json:"nodeRef,omitempty"`
	Addresses []string   `json:"addresses,omitempty"`
	IssuedAt  time.Time  `json:"issuedAt"`
	BoundAt   *time.Time `json:"boundAt,omitempty"`
}

HomeVPNPeer is one device's peer of a coordinated tool. NodeRef is the tool's node (Headscale node ID, Tailscale device ID, Pangolin client ID), KeyRef its join key. Neither is a secret.

func BindHomeVPNPeer

func BindHomeVPNPeer(root, deviceID, tool string, node HomeVPNLiveNode, now time.Time) (HomeVPNPeer, error)

BindHomeVPNPeer binds a coordinated node to the active device whose probe arrived from it through the VPN. A node bound to another device, a node joined with another device's key and a device without a current peer are refused.

func HomeVPNPeerOf

func HomeVPNPeerOf(root, deviceID, tool string) (HomeVPNPeer, error)

HomeVPNPeerOf returns the device's current peer of a tool.

func IssueHomeVPNPeer

func IssueHomeVPNPeer(root, deviceID, tool string, now time.Time, issue func() (HomeVPNPeer, error)) (HomeVPNPeer, error)

IssueHomeVPNPeer gives an active device a new join key of a coordinated tool. The device is checked under the ledger lock and issue runs only for an active device; a peer the device held before is marked for removal, so a device holds at most one peer.

func ListHomeVPNPeers

func ListHomeVPNPeers(root string) ([]HomeVPNPeer, error)

ListHomeVPNPeers returns the ledger.

func RemoveAllHomeVPNPeers

func RemoveAllHomeVPNPeers(root, tool string) ([]HomeVPNPeer, error)

RemoveAllHomeVPNPeers marks every peer of a tool for removal (the Owner switches the add-on off) and returns them.

type HomeVPNRemovals

type HomeVPNRemovals struct {
	Nodes   []HomeVPNLiveNode `json:"nodes"`
	Keys    []string          `json:"keys"`
	Settled []HomeVPNPeer     `json:"settled"`
}

HomeVPNRemovals is what the tool must delete now: nodes and join keys. Settled are the ledger entries to forget once both succeeded.

func ReconcileHomeVPNPeers

func ReconcileHomeVPNPeers(root, tool string, live []HomeVPNLiveNode, now time.Time) (HomeVPNRemovals, error)

ReconcileHomeVPNPeers compares the tool's live nodes with the registry. It binds a node that joined with a device's own key, marks the peers of inactive devices and expired join keys for removal, and returns every node and key the tool must delete now: those of entries being removed, nodes no active device bound within the grace, and, because a Tailscale node does not name its key, every unbound node that appeared after a removed device's unbound key was issued.

type HomeVPNSelection

type HomeVPNSelection struct {
	Schema    string    `json:"schema"`
	Tool      string    `json:"tool"`
	EnabledAt time.Time `json:"enabledAt"`
	// Endpoint is the Owner-declared host:port devices dial from away: the
	// WireGuard UDP endpoint, or the peer service that carries Headscale's
	// control endpoint. Tailscale and Pangolin have none.
	Endpoint  string                     `json:"endpoint,omitempty"`
	Tailscale *HomeVPNTailscaleSelection `json:"tailscale,omitempty"`
	Pangolin  *HomeVPNPangolinSelection  `json:"pangolin,omitempty"`
}

HomeVPNSelection is the Owner's opt-in: the one tool and what it needs that is not secret. Secrets of a tool (an OAuth client, an API key, Newt's secret) are separate custody files of the tool, never in this record.

func LoadHomeVPNSelection

func LoadHomeVPNSelection(root string) (HomeVPNSelection, error)

LoadHomeVPNSelection reads the Owner's opt-in; ErrHomeVPNOff without one.

type HomeVPNTailscaleSelection

type HomeVPNTailscaleSelection struct {
	Tailnet string `json:"tailnet"`
}

HomeVPNTailscaleSelection names the Owner's tailnet.

type HomeVerifier

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

HomeVerifier verifies device credentials and DPoP proofs for the services of this server and renews credentials on the Authority. It keeps the replay state of every proof it accepted until the proof's window has passed, so one long-lived verifier serves a process; use HomeVerifierFor. A proof whose time, less the clock leeway, is not after the verifier's start is refused, so a restart cannot reopen a replay; the cost is that proofs of the first seconds after a start are refused and the client retries.

func HomeVerifierFor

func HomeVerifierFor(root string) (*HomeVerifier, error)

HomeVerifierFor returns the process-wide verifier of a workspace.

func (*HomeVerifier) Authenticate

func (v *HomeVerifier) Authenticate(request HomeDeviceRequest) (HomeAuthentication, error)

Authenticate admits one request only when its credential is a current credential of an active registry entry and its proof is a fresh, unused DPoP proof signed by the credential's key for exactly this method, route origin and path. Every refusal is ErrHomeAccessDenied. While the registry cannot be refreshed, issued credentials stay valid to their lifetime against the last-known state; a device the verifier has never seen stays refused.

func (*HomeVerifier) AuthenticateLaunch

func (v *HomeVerifier) AuthenticateLaunch(launch HomeLaunch) (HomeAuthentication, error)

AuthenticateLaunch admits one launch proof: signed by the key of an active device whose current credential it presents (ath), for GET on exactly this host's launch path, at most 60 seconds old, used once, and bound to the Gate's launch state and the target on this host. A state the Gate did not set in this browser is refused, so nobody can log a browser in as someone else.

func (*HomeVerifier) ConfirmEnrollmentFromDevice

func (v *HomeVerifier) ConfirmEnrollmentFromDevice(caller HomeAuthentication, deviceID, challenge, idToken string) (HomeDevice, error)

ConfirmEnrollmentFromDevice confirms the pending enrollment deviceID from an enrolled device: one of the same person's (the confirmer while the person has one), or else one of the Owner's (D-2026-11-02-11: own device first, the Owner otherwise). caller is the result of Authenticate for the confirming request; idToken is the confirmer's passkey step-up through HomeDeviceClientID for the entry's enroll-confirm action and challenge.

func (*HomeVerifier) Enroll

Enroll decides one enrollment submission. It verifies the proof of possession and the person's fresh passkey sign-in against the current Home trust, applies the rate limit and the device cap of the applied #DeviceEnrollmentPolicy, and records the entry: active with its first credential at home, pending away. Every failure before the decision is ErrHomeAccessDenied; the decision's own refusals are their named errors. It fails closed whenever the trust, the registry or the policy is not current (partition rule).

func (*HomeVerifier) RenewDeviceCredential

func (v *HomeVerifier) RenewDeviceCredential(request HomeRenewalRequest) (HomeDeviceCredential, error)

RenewDeviceCredential signs a new one-hour credential for an active registry entry whose key signed the proof. After 24 hours without user verification it returns ErrHomeUserVerificationRequired unless the request carries a valid passkey assertion; every other refusal is ErrHomeAccessDenied. It runs only on the Authority (the holder of the device issuer key and registry) and fails closed whenever the registry, or for a user verification the Pocket ID pin, is not current. A key that is not an active registry entry is refused before it reaches replay state or the registry lock.

func (*HomeVerifier) VerifyHomeAppPass

func (v *HomeVerifier) VerifyHomeAppPass(secret, host string) (HomeAppPass, error)

VerifyHomeAppPass admits a pass only for the host it was issued for, before its end, unrevoked, and while its device is an active registry entry of its account.

func (*HomeVerifier) VerifyHomeBrowserSession

func (v *HomeVerifier) VerifyHomeBrowserSession(value, host string, remote bool) (HomeBrowserSession, error)

VerifyHomeBrowserSession admits a session value only for the host it was issued for, before its end, and while its device is an active registry entry of its account, so revoking the device ends its sessions within the registry refresh. Away (remote) a home session counts only for the away lifetime.

type OwnerProvisioner

type OwnerProvisioner struct {
	// Client is the PocketID admin-API client (real or fake).
	Client PocketIDClient

	// PocketIDURL is the public origin of the PocketID instance, used to
	// build SetupURL. Example: "https://id.example.com" (no trailing slash).
	PocketIDURL string

	// OwnersGroup is the name of the group new owners are added to.
	// Defaults to "owners" when empty.
	OwnersGroup string
}

OwnerProvisioner creates the owner record in PocketID and adds them to the owners group, then issues a one-time-access token for WebAuthn enrollment.

func (*OwnerProvisioner) Provision

func (p *OwnerProvisioner) Provision(ctx context.Context, spec OwnerSpec) (*ProvisionResult, error)

Provision creates the owner user in PocketID, adds them to the owners group, and returns a ProvisionResult containing the user's UUID and a setup URL the owner clicks once to enroll a WebAuthn credential.

Source=="local" is the only locally provisioned path. Source=="cloud" returns an orchestrator-managed error. Any other Source value is rejected as invalid.

type OwnerSpec

type OwnerSpec struct {
	// Source controls the provisioning path. This local provisioner supports
	// "local" only; "cloud" is handled by TechStack/kombify Cloud.
	Source string

	// Email is the owner's address (also used as the WebAuthn account label).
	Email string

	// Username is the PocketID login handle.
	Username string

	// DisplayName is what PocketID renders in the UI; defaults to Username
	// when empty. PocketID v2 stores this in the FirstName column on the
	// underlying user record.
	DisplayName string

	// ForeignSubjectID is the external IdP subject for Source=="cloud".
	// Ignored when Source=="local".
	ForeignSubjectID string
}

OwnerSpec describes the daily-admin owner of a homelab instance.

type PocketIDClient

type PocketIDClient interface {
	CreateUser(ctx context.Context, req pocketid.CreateUserRequest) (*pocketid.User, error)
	AddUserToGroup(ctx context.Context, userID, groupID string) error
	GetGroupIDByName(ctx context.Context, groupName string) (string, error)
	CreateOneTimeAccessToken(ctx context.Context, userID string, ttl time.Duration) (string, error)
}

PocketIDClient is the subset of pocketid.Client used by provisioners. Defined as an interface so tests can supply fakes without standing up an HTTP server.

type ProvisionResult

type ProvisionResult struct {
	// UserID is the PocketID-assigned UUID for the new owner record.
	UserID string

	// SetupURL is the one-time link the owner clicks to enroll a WebAuthn
	// credential. It embeds a single-use token with a 24-hour TTL. Local
	// source only.
	SetupURL string
}

ProvisionResult is what Provision returns on success.

type RecoveryDeviceRequest

type RecoveryDeviceRequest struct {
	Method string
	Proof  string
	Code   string
}

RecoveryDeviceRequest is a recovery call by an enrolled Owner device: a DPoP proof by its registry key for the exact route, and the recovery code where the route takes one. No device credential is needed: the device may no longer pass a user verification once the passkey is gone.

type RecoveryNotice

type RecoveryNotice struct {
	ID         string     `json:"id"`
	Kind       string     `json:"kind"`
	Account    string     `json:"account"`
	Devices    []string   `json:"devices"`
	Origin     string     `json:"origin"`
	RequestID  string     `json:"requestId,omitempty"`
	EligibleAt *time.Time `json:"eligibleAt,omitempty"`
	CreatedAt  time.Time  `json:"createdAt"`
}

RecoveryNotice is one recovery event addressed to the Owner devices that were enrolled when it happened. Delivery (Companion Feed, Hub Web Push, State Console) reads these records; they hold no secret.

func DeviceRecoveryNotices

func DeviceRecoveryNotices(root string, request RecoveryDeviceRequest, now time.Time) ([]RecoveryNotice, error)

DeviceRecoveryNotices returns, newest first, the notices addressed to the enrolled Owner device whose key signed the proof (GET with DPoP), so an Owner device learns of a recovery started elsewhere within its 24 hours.

func ListRecoveryNotices

func ListRecoveryNotices(root string) ([]RecoveryNotice, error)

ListRecoveryNotices returns the recovery notices, newest first. This is the record igti.6 delivers to every listed Owner device.

The recovery state stays in the host's custody; its notices, which hold no secret, are projected into the verification custody (recoveryNoticesProjectionPath) on every write, so the core's Hub delivers them by Web Push and lists them to Owner devices without reading the state.

type RecoveryPasskeyIssuer

type RecoveryPasskeyIssuer func(ctx context.Context, subject string) (RecoveryPasskeyLink, error)

RecoveryPasskeyIssuer mints the one-time link for the bound Owner subject.

func LocalRecoveryPasskeyIssuer

func LocalRecoveryPasskeyIssuer(root string) RecoveryPasskeyIssuer

LocalRecoveryPasskeyIssuer mints the Owner's one-time Pocket ID link through the Authority's local Pocket ID admin API, for the public identity origin.

type RecoveryPasskeyLink struct {
	URL       string    `json:"passkeyRegistrationUrl"`
	ExpiresAt time.Time `json:"expiresAt"`
}

RecoveryPasskeyLink is the Pocket ID one-time link a redeemed code yields.

func CompleteDeviceRecovery

func CompleteDeviceRecovery(ctx context.Context, root string, request RecoveryDeviceRequest, issue RecoveryPasskeyIssuer, now time.Time) (RecoveryPasskeyLink, error)

CompleteDeviceRecovery finishes a pending device recovery: the same device key, the same valid code, no earlier than 24 hours after the request and before it expired. The code is consumed only after the link exists.

func RedeemRecoveryCodeAtConsole

func RedeemRecoveryCodeAtConsole(ctx context.Context, root, code string, issue RecoveryPasskeyIssuer, now time.Time) (RecoveryPasskeyLink, error)

RedeemRecoveryCodeAtConsole is the local console path: a valid, unconsumed code yields the Owner's passkey registration link at once. Any pending device recovery ends. The code is consumed only after the link exists.

type RecoveryRequest

type RecoveryRequest struct {
	ID          string    `json:"id"`
	Device      string    `json:"device"`
	Account     string    `json:"account"`
	RequestedAt time.Time `json:"requestedAt"`
	EligibleAt  time.Time `json:"eligibleAt"`
	ExpiresAt   time.Time `json:"expiresAt"`
}

RecoveryRequest is a pending recovery started from an enrolled Owner device.

func RequestDeviceRecovery

func RequestDeviceRecovery(root string, request RecoveryDeviceRequest, now time.Time) (RecoveryRequest, error)

RequestDeviceRecovery starts the delayed path from an enrolled Owner device: the proof's key must be an active registry entry of the Owner's account and the code must be valid. The recovery becomes eligible after 24 hours; every Owner device receives a notice and the console or any Owner device can cancel it meanwhile.

type RecoverySecrets

type RecoverySecrets struct {
	// RecoveryKey decrypts every recovery set ("AGE-SECRET-KEY-PQ-1...").
	RecoveryKey string `json:"recoveryKey"`
	// RecoveryCode registers a new Owner passkey once.
	RecoveryCode string `json:"recoveryCode"`
	// Recipient is the public half the Authority keeps to write sets.
	Recipient string `json:"recipient"`
}

RecoverySecrets are shown to the Owner once and never persisted.

func SetupRecovery

func SetupRecovery(root string, rotate bool, now time.Time) (RecoverySecrets, error)

SetupRecovery creates the recovery key and code once and returns both for a single display; only the public recipient and a slow verifier of the code are stored. With rotate it replaces an existing key and code (the Owner lost one of them but still runs the Authority): every pending device recovery ends and sets written before stay readable only with the old key.

type RecoverySetEntry

type RecoverySetEntry struct {
	Path   string `json:"path"`
	Dir    bool   `json:"dir,omitempty"`
	Mode   uint32 `json:"mode"`
	UID    int    `json:"uid"`
	GID    int    `json:"gid"`
	Size   int64  `json:"size"`
	SHA256 string `json:"sha256,omitempty"`
}

RecoverySetEntry is one file or directory of the set.

type RecoverySetManifest

type RecoverySetManifest struct {
	Schema         string              `json:"schema"`
	CreatedAt      time.Time           `json:"createdAt"`
	OwnerRef       string              `json:"ownerRef"`
	SiteRef        string              `json:"siteRef"`
	NodeRef        string              `json:"nodeRef"`
	IdentityOrigin string              `json:"identityOrigin"`
	Recipient      string              `json:"recipient"`
	Files          []RecoverySetEntry  `json:"files"`
	Volumes        []RecoverySetVolume `json:"volumes"`
}

RecoverySetManifest describes one set. It holds no secret.

func WriteRecoverySet

func WriteRecoverySet(root string, volumes []RecoveryVolumeSource, out io.Writer, now time.Time) (RecoverySetManifest, error)

WriteRecoverySet streams the Authority's identity custody and the given runtime volumes as one encrypted set to out. The Pocket ID volume is required; the step-ca database exists on kits that run step-ca.

type RecoverySetRestore

type RecoverySetRestore struct {
	Manifest RecoverySetManifest
	// contains filtered or unexported fields
}

RecoverySetRestore is an opened, fully verified set staged below the workspace. The caller places its volumes into fresh Docker volumes, then installs its custody. Close removes the stage.

func OpenRecoverySet

func OpenRecoverySet(root string, encrypted io.Reader, recoveryKey string) (*RecoverySetRestore, error)

OpenRecoverySet opens a set with the Owner's recovery key and verifies it completely (age decryption, manifest MAC, every digest, the staged Owner custody and identity origin) without changing the workspace. It refuses a workspace that holds another Owner or another identity origin.

func (*RecoverySetRestore) Close

func (r *RecoverySetRestore) Close() error

Close removes the staged volumes.

func (*RecoverySetRestore) InstallCustody

func (r *RecoverySetRestore) InstallCustody() error

InstallCustody installs the set's custody into the workspace, keeping the identity origin: the restored runtime custody carries the same Pocket ID host, so passkeys registered for it keep working. The restored recovery state ends any pending device recovery and marks the code used (ResetRecoveryAfterRestore). Installing the same set again is allowed.

func (*RecoverySetRestore) PlaceVolume

func (r *RecoverySetRestore) PlaceVolume(name, target string) error

PlaceVolume copies one staged volume into an empty target directory (the mountpoint of a freshly created Docker volume), keeping modes, owners when running as root, and sparse regions.

type RecoverySetVolume

type RecoverySetVolume struct {
	Name         string             `json:"name"`
	DockerVolume string             `json:"dockerVolume"`
	Entries      []RecoverySetEntry `json:"entries"`
}

RecoverySetVolume is one Docker volume of the identity runtime.

type RecoveryStatus

type RecoveryStatus struct {
	Configured   bool             `json:"configured"`
	Recipient    string           `json:"recipient,omitempty"`
	ConfiguredAt time.Time        `json:"configuredAt,omitzero"`
	CodeIssuedAt time.Time        `json:"codeIssuedAt,omitzero"`
	CodeConsumed bool             `json:"codeConsumed"`
	Pending      *RecoveryRequest `json:"pending,omitempty"`
}

RecoveryStatus is the secret-free recovery state of the Authority.

func LoadRecoveryStatus

func LoadRecoveryStatus(root string) (RecoveryStatus, error)

LoadRecoveryStatus reads the secret-free recovery state.

type RecoveryVolumeSource

type RecoveryVolumeSource struct {
	Name         string
	DockerVolume string
	Dir          string
}

RecoveryVolumeSource is one identity runtime volume read from its host directory while its writer is paused.

Jump to

Keyboard shortcuts

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