identitycli

package
v0.2.1 Latest Latest
Warning

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

Go to latest
Published: Jul 1, 2026 License: MIT Imports: 20 Imported by: 0

Documentation

Overview

Package identitycli holds the business logic behind the `vaultmind identity` commands (init, sign), keeping the cmd/ layer ultra-thin (ADR-001).

The cardinal property: this package is KEYLESS for signing. SignEntry never opens the private-key file — it validates+canonicalizes via the slice-1 identity core and delegates the actual ed25519 sign to a SignerClient (the separate signer process over its 0600 socket). Only Init touches a private key, and only to SEAL it via signer.SealPrivateKey; the key is never printed or logged.

Index

Constants

View Source
const (

	// EnrollConfirmPrompt is shown on errOut to ask the member to confirm the
	// fingerprint matches the one the admin gave them out of band.
	EnrollConfirmPrompt = "Confirm this matches the fingerprint your admin gave you out-of-band [y/N]: "
	// EnrollAutoConfirmedNote is printed instead of the prompt when --yes is set.
	EnrollAutoConfirmedNote = "auto-confirmed via --yes"
	// EnrollConfirmedNote is the interactive-confirm result word shown in the
	// summary when the member accepted the OOB fingerprint prompt.
	EnrollConfirmedNote = "confirmed"
	// EnrollNetworkLabel prefixes the printed network id.
	EnrollNetworkLabel = "network: "
	// EnrollRelayLabel prefixes the printed relay base URL.
	EnrollRelayLabel = "relay: "
	// EnrollFingerprintLabel prefixes the printed fingerprint (= network id).
	EnrollFingerprintLabel = "fingerprint: "
	// EnrollSignedNote confirms the request signed AND self-verified
	// (proof-of-possession passed).
	EnrollSignedNote = "✓ signed & self-verified"
	// EnrollNextStepNote tells the member to hand the emitted request to the admin.
	EnrollNextStepNote = "hand this to your admin out-of-band; they run `vaultmind identity enroll-add`."
	// EnrollAnchorPinnedNote confirms the OOB-confirmed network root was persisted
	// so a future `vaultmind doctor` authenticates registry verification against it.
	EnrollAnchorPinnedNote = "✓ pinned this network's root (a future `vaultmind doctor` will authenticate against it)"
)

Enroll output-label / prompt constants (SSOT) — every printed string the member-enroll flow emits is a named constant here, never an inline literal.

View Source
const (
	// ErrEnrollEmptyInvite is returned when --invite is empty.
	ErrEnrollEmptyInvite = "identitycli: --invite is required (a vmenroll1: token or enroll URL)"
	// ErrEnrollEmptyDisplayName is returned when --display-name is empty.
	ErrEnrollEmptyDisplayName = "identitycli: --display-name is required"
	// ErrEnrollEmptySlug is returned when --slug is empty.
	ErrEnrollEmptySlug = "identitycli: --slug is required"
	// ErrEnrollEmptyPubKey is returned when --pubkey is empty.
	ErrEnrollEmptyPubKey = "identitycli: --pubkey is required (your base64-std ed25519 identity pubkey)"
	// ErrEnrollEmptyTransportPubKey is returned when --transport-pubkey is empty.
	ErrEnrollEmptyTransportPubKey = "identitycli: --transport-pubkey is required (your base64-std WireGuard pubkey)"
	// ErrEnrollEmptyAnchorPath is returned when AnchorStorePath is empty. The cmd
	// layer always resolves it (xdg.DataFile); an empty value is a PROGRAMMING
	// error, surfaced fail-closed before any network/signer interaction.
	ErrEnrollEmptyAnchorPath = "identitycli: AnchorStorePath is empty (programming error: the cmd layer must set it)"

	// ErrEnrollSelfVerify is returned when proof-of-possession self-verification
	// fails: the running signer holds a DIFFERENT key than --pubkey.
	ErrEnrollSelfVerify = "" /* 168-byte string literal not displayed */
	// ErrEnrollPersistAnchor wraps a failure to persist the OOB-confirmed trust
	// anchor. Enroll FAILS CLOSED on it: the request is NOT emitted, so there is
	// never a half-state (a request handed to the admin without the pinned root).
	ErrEnrollPersistAnchor = "identitycli: persist network trust anchor"
)

Enroll error strings (SSOT). Each failure mode is a distinct typed reject so the member can tell a transport/cross-check/proof failure apart.

View Source
const (

	// EnrollAddAddedLabel prefixes the added binding's slug.
	EnrollAddAddedLabel = "added: "
	// EnrollAddPubKeyLabel prefixes the added binding's pubkey.
	EnrollAddPubKeyLabel = "pubkey: "
	// EnrollAddKeyEpochLabel prefixes the added binding's key_epoch.
	EnrollAddKeyEpochLabel = "key_epoch: "
	// EnrollAddEpochLabel prefixes the new registry epoch.
	EnrollAddEpochLabel = "new registry epoch: "
	// EnrollAddWindowLabel prefixes the issuance window (valid_from..valid_until).
	EnrollAddWindowLabel = "issuance window: "
	// EnrollAddNextStepNote tells the admin to root-sign the emitted registry.
	EnrollAddNextStepNote = "NEXT: run the ROOT signer and pipe this to `vaultmind identity sign-registry`."
	// EnrollAddTransportNote surfaces that transport fields are not yet carried
	// into the binding (WireGuard wiring is a later slice).
	EnrollAddTransportNote = "NOTE: transport_pubkey/transport_endpoint are NOT yet carried into the binding (WireGuard = later slice)."
)

enroll-add output-label / guidance constants (SSOT) — every printed string the admin enroll-add flow emits is a named constant here, never an inline literal.

View Source
const (
	// ErrEnrollAddEmptyRequest is returned when the request input is empty.
	ErrEnrollAddEmptyRequest = "identitycli: enrollment request is empty"

	// ErrEnrollAddPoP is returned when proof-of-possession fails: the request sig
	// does not verify under its own pubkey.
	ErrEnrollAddPoP = "identitycli: proof-of-possession failed: the request signature does not verify under its pubkey"

	// ErrEnrollAddNoNetwork is returned when neither --root-pubkey nor
	// --network-id is supplied (cannot resolve the admin network).
	ErrEnrollAddNoNetwork = "identitycli: at least one of --root-pubkey or --network-id is required to resolve the admin network"
	// ErrEnrollAddNetworkSpecifierDisagree is returned when --root-pubkey-derived
	// network and --network-id are both supplied but disagree.
	ErrEnrollAddNetworkSpecifierDisagree = "identitycli: --root-pubkey-derived network and --network-id disagree"
	// ErrEnrollAddCrossNetwork is returned when the request's network_id is not
	// the admin network (cross-network request refused).
	ErrEnrollAddCrossNetwork = "identitycli: cross-network request refused"

	// ErrEnrollAddSignedNeedsRootPubKey is returned when a signed-envelope
	// --registry is supplied without --root-pubkey (cannot integrity-verify).
	ErrEnrollAddSignedNeedsRootPubKey = "identitycli: a signed-envelope --registry requires --root-pubkey to integrity-verify it"
	// ErrEnrollAddUntrustedRegistry is returned when a signed-envelope --registry
	// root signature does not verify (refusing to mutate untrusted state).
	ErrEnrollAddUntrustedRegistry = "identitycli: current registry root signature invalid — refusing to mutate untrusted state"
	// ErrEnrollAddUnrecognizedRegistry is returned when the --registry input is
	// neither a signed envelope nor an unsigned wireRegistry.
	ErrEnrollAddUnrecognizedRegistry = "identitycli: unrecognized registry shape (expected an unsigned wireRegistry or a signed distribution envelope)"
	// ErrEnrollAddLiveSlugExists is returned when the slug already has a live
	// binding (rotation is out of scope for v1; revoke before re-adding).
	ErrEnrollAddLiveSlugExists = "identitycli: slug already has a live binding; revoke before re-adding"
	// ErrEnrollAddTupleExists is returned when the {slug,key_epoch} tuple already
	// exists in the current registry.
	ErrEnrollAddTupleExists = "identitycli: a binding with this {slug,key_epoch} already exists"
	// ErrEnrollAddEpochOverflow is returned when bumping the epoch would exceed
	// the JCS-safe ceiling (MaxSafeEpoch).
	ErrEnrollAddEpochOverflow = "identitycli: new registry epoch exceeds the JCS-safe ceiling"
)

enroll-add error strings (SSOT). Each failure mode is a distinct typed reject.

View Source
const (
	// PubKeyLabel prefixes the printed public key. Only the PUBLIC key is ever
	// printed by Init.
	PubKeyLabel = "public_key (ed25519, base64): "
	// SigLabel prefixes the printed signature.
	SigLabel = "signature (ed25519, base64): "
)

Output label constants (SSOT) so cmd/ and tests reference one definition.

View Source
const (
	// ErrEnvelopeParse is returned by SignEnvelope when the envelope JSON cannot
	// be decoded (bad JSON, unknown field, or trailing data — strict, fail closed).
	ErrEnvelopeParse = "identitycli: parse envelope JSON"
	// ErrEnvelopeBadFromPubKey is returned when fromPubKeyB64 is set but not valid
	// base64 of an ed25519 public key.
	ErrEnvelopeBadFromPubKey = "identitycli: from_pubkey must be base64 of an ed25519 public key"
)

Envelope-signing error strings (SSOT).

View Source
const (
	// InviteTokenLabel prefixes the printed bootstrap token. It is an OUTPUT label,
	// not a secret; the regex secret-scanner false-positives because the value ends
	// just before the next label's opening quote.
	InviteTokenLabel = "token: " // nosemgrep: go-hardcoded-secret -- output label, not a credential
	// InviteURLLabel prefixes the printed enroll URL (token in the fragment).
	InviteURLLabel = "url: "
	// InviteFingerprintLabel prefixes the printed OOB fingerprint (the network id).
	InviteFingerprintLabel = "fingerprint (share out-of-band): "
	// InviteConfirmNote is the one-line instruction printed under the fingerprint:
	// the member MUST confirm this value with the admin over a TRUSTED channel.
	InviteConfirmNote = "The member must confirm this fingerprint with you over a trusted channel before enrolling."
)

Invite output label constants (SSOT) so cmd/ and tests reference one definition for the three printed blocks plus the out-of-band confirm note.

View Source
const (
	// ErrEnrollmentParse is returned by SignEnrollment when the enrollment JSON
	// cannot be decoded (bad JSON, unknown field, or trailing data — strict, fail
	// closed).
	ErrEnrollmentParse = "identitycli: parse enrollment JSON"
	// ErrEnrollmentMarshalResult wraps a marshal failure of the sign result.
	ErrEnrollmentMarshalResult = "identitycli: marshal sign-enrollment result"
)

Enrollment-signing error strings (SSOT).

View Source
const (
	// ErrRegistryParse is returned by SignRegistry when the registry JSON cannot
	// be decoded (bad JSON, unknown field, or trailing data — strict, fail closed).
	ErrRegistryParse = "identitycli: parse registry JSON"
	// ErrRegistryBadPubKey is returned when a binding's pubkey is not valid
	// base64-std of a validatable ed25519 public key.
	ErrRegistryBadPubKey = "identitycli: binding pubkey must be base64-std of an ed25519 public key"
	// ErrRegistryMarshalDist wraps a distribution-envelope marshal failure.
	ErrRegistryMarshalDist = "identitycli: marshal distribution envelope"
	// ErrRegistryRevokedAtNegative is returned when a binding's revoked_at is
	// negative (a pre-epoch revocation timestamp is nonsensical — fail closed).
	ErrRegistryRevokedAtNegative = "identitycli: binding revoked_at must not be negative"
)

Registry-signing error strings (SSOT).

View Source
const ErrInviteBadRootPubKey = "identitycli: root pubkey must be base64-std of an ed25519 public key"

ErrInviteBadRootPubKey is returned by Invite when rootPubKeyB64 is not base64-std of a valid 32-byte ed25519 public key (bad base64 / wrong length / small-order).

Variables

This section is empty.

Functions

func Enroll

func Enroll(out, errOut io.Writer, in io.Reader, cfg EnrollConfig) error

Enroll runs the Contract-B member-onboarding flow: decode the invite, fetch the relay's well-known root, CROSS-CHECK it against the invite (the security spine), confirm the fingerprint out of band, assemble + gate + sign the enrollment request via the keyless signer, self-verify proof-of-possession, and emit the signed wire JSON on out (human guidance goes to errOut). It FAILS CLOSED: any cross-check, gate, sign, or self-verify failure returns non-nil and emits NO request.

func EnrollAdd

func EnrollAdd(out, errOut io.Writer, in io.Reader, cfg EnrollAddConfig) error

EnrollAdd is the Contract-B ADMIN counterpart to `identity enroll`. It reads a member's signed enrollment-request wire JSON, verifies proof-of-possession, checks the request is for the admin's OWN network, integrity-verifies the current registry (when signed), pre-checks uniqueness FAIL-CLOSED, appends the new binding with a bumped epoch + refreshed issuance window, and emits the UPDATED UNSIGNED registry (the input `sign-registry` root-signs). It does NOT sign — that is the established two-verb split.

It FAILS CLOSED: any read/parse/PoP/network/integrity/uniqueness/epoch error returns non-nil and emits NOTHING (never a partial registry).

func Init

func Init(out io.Writer, keyPath string) error

Init mints a per-agent ed25519 keypair, SEALs the private key to keyPath (0600, refusing to overwrite), and writes ONLY the public key to out. The private key is never printed or logged.

func Invite

func Invite(out io.Writer, rootPubKeyB64, relay string) error

Invite builds a Contract-B agent-network INVITE from the network's root public key (base64-std) and relay base URL, then prints three clearly-labelled blocks — the token, the enroll URL, and the OOB fingerprint (with a confirm note) — to out. The invite is UNSIGNED by design: it carries the root anchor itself, and trust comes from the member confirming the fingerprint out of band.

It FAILS CLOSED: a bad/invalid root pubkey or empty relay returns a non-nil error and prints nothing partial.

func SignEnrollment

func SignEnrollment(out io.Writer, client enrollment.SignerClient, enrollmentJSON []byte) error

SignEnrollment reads an agent-enrollment request (the signed-subset fields), enforces the Contract-B enrollment gates + canonicalizes via the enrollment package, signs the canonical bytes through the KEYLESS SignerClient, and writes {sig, pubkey} as JSON to out (pubkey is ECHOED from the request — it IS the signed proof-of-possession key). It is KEYLESS and FAILS CLOSED: any parse, gate, canonicalize, or signer error returns non-nil and prints nothing.

func SignEntry

func SignEntry(out io.Writer, client SignerClient, entryJSON []byte) error

SignEntry runs the slice-1 validate+canonicalize path, hands the canonical bytes to the SignerClient, and writes the signature to out. It is provably KEYLESS: it has no key path and never reads the private-key file. It FAILS CLOSED — any schema, canonicalize, or signer error returns non-nil and prints nothing.

func SignEnvelope

func SignEnvelope(out io.Writer, client SignerClient, envelopeJSON []byte, fromPubKeyB64 string) error

SignEnvelope reads a chat-message envelope (the signed-subset fields), enforces the Contract-B signing gates + canonicalizes via the envelope package, signs the canonical bytes through the KEYLESS SignerClient, and writes {sig, from_pubkey, key_epoch} as JSON to out. fromPubKeyB64 (optional) is the signer's public key stamped into the from_pubkey HINT — it is DERIVED, not signed, and the verifier ignores it. It is KEYLESS and FAILS CLOSED: any parse, gate, canonicalize, or signer error returns non-nil and prints nothing.

func SignRegistry

func SignRegistry(out io.Writer, client registry.RegistrySigner, registryJSON []byte) error

SignRegistry reads an UNSIGNED registry (the registry.Registry shape), signs its JCS-canonical bytes through the KEYLESS SignerClient (the root key stays in the signer), assembles the distribution envelope via the registry package, and writes the envelope JSON to out. It is KEYLESS and FAILS CLOSED: any parse, bad pubkey, epoch-gate, signer, or marshal error returns non-nil and prints nothing (never a partial/unsigned result).

Types

type EnrollAddConfig

type EnrollAddConfig struct {
	// RootPubKeyB64 is the base64-std root ed25519 pubkey. REQUIRED to
	// integrity-verify a signed-envelope --registry; also used to derive the
	// admin network id.
	RootPubKeyB64 string
	// NetworkID is an alternative admin-network specifier (vmnet1:…). At least one
	// of RootPubKeyB64 / NetworkID is required; when both are set they must agree.
	NetworkID string
	// RegistryInput is the current registry: an unsigned wireRegistry JSON OR a
	// signed distribution envelope (auto-detected). nil/empty => fresh registry.
	RegistryInput []byte
	// ValiditySeconds is the registry+binding issuance window in seconds (string
	// so the flag layer can stay on a plain string flag). Empty => one year.
	ValiditySeconds string
	// OriginDaemons is the comma-separated AuthorizedOriginDaemons list (parsed
	// here). Empty => no authorized origin daemons.
	OriginDaemons string

	// Now is the timestamp source (nil => time.Now). Injected for deterministic
	// issuance windows in tests.
	Now func() time.Time
}

EnrollAddConfig carries everything the admin enroll-add flow needs. The registry input is passed as raw bytes (the cmd layer reads --registry from a file or leaves it nil for a fresh registry), and the request is read from the supplied reader (the cmd resolves file-vs-stdin). The clock is injectable for deterministic timestamps in tests.

type EnrollConfig

type EnrollConfig struct {
	InviteTokenOrURL   string // --invite (a vmenroll1: token OR an enroll URL)
	DisplayName        string // --display-name
	Slug               string // --slug
	PubKeyB64          string // --pubkey (base64-std ed25519 identity pubkey)
	TransportPubKeyB64 string // --transport-pubkey (base64-std WireGuard pubkey)
	TransportEndpoint  string // --transport-endpoint (optional host:port; "" => omitted)
	SignerSocket       string // --signer-socket

	// AnchorStorePath is the file the OOB-confirmed trust anchor is pinned to. It
	// is NOT a user flag — the cmd layer resolves it via defaultNetworkAnchorPath()
	// (xdg.DataFile, beside the signer key). Empty is a programming error.
	AnchorStorePath string

	AssumeYes bool // --yes (skip the OOB fingerprint confirmation prompt)

	// Seams (nil => production default).
	HTTPClient *http.Client            // well-known fetch client
	Signer     enrollment.SignerClient // keyless signer (default: &signer.Client{SocketPath})
	Now        func() time.Time        // created timestamp source (default: time.Now)
	RandReader io.Reader               // nonce entropy source (default: crypto/rand.Reader)
}

EnrollConfig carries everything the member-enroll flow needs, with injectable seams so tests need no real daemon, network, or clock. Every seam defaults to the production implementation when nil/zero.

type SignerClient

type SignerClient interface {
	Sign(canonicalBytes []byte) ([]byte, error)
}

SignerClient is the minimal seam SignEntry needs: hand it canonical bytes, get a signature back. The real implementation is *signer.Client; tests inject a fake to prove the sign path never opens the key file.

Jump to

Keyboard shortcuts

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