Documentation
¶
Overview ¶
Package registry implements Contract-B SLICE 3: the trust-root REGISTRY itself — a root-signed, epoched, freshness-bounded slug→pubkey binding set plus the consumer resolve/verify surface.
It builds STRICTLY on the slice-1 primitives (identity.Canonicalize for JCS, identity.VerifyCanonical for the small-order-rejecting cofactorless strict verify, and identity.ValidateSchema via the SignedEntry gate) and does not reimplement canonicalization or verification.
The trust model (ratified in docs/mesh/contractb-decisions.md):
- The registry is signed by an OFFLINE root key (dev: an in-process keypair). SignRegistry is the offline-root operation; VerifyAndLoad is the load-bearing consumer trust gate.
- ANTI-ROLLBACK: a monotonic epoch; the consumer persists the highest epoch seen and refuses any registry at-or-below it.
- FRESHNESS / anti-freeze-eclipse: a registry past valid_until OR older than maxStaleness FAILS CLOSED (a stale registry may hide a revocation, so it must not be trusted).
- REVOCATION + ROTATION: a binding carries revoked_at; rotation is a new {pubkey,key_epoch} tuple with the old tuple revoked. The ed25519 pubkey is identity; resolve/verify default-deny anything revoked, expired, or epoch-mismatched.
SLICE-3 scope is the registry MECHANISM only. The agent-chat --registry distribution wiring and the WireGuard daemon keys are deferred to slice 4.
Index ¶
- Constants
- func Freshness(env SignedRegistry) (validFrom, validUntil int64, err error)
- func IsStaleAt(validFrom, validUntil int64, now time.Time, maxStaleness time.Duration) bool
- func MarshalDistribution(env SignedRegistry) ([]byte, error)
- func NetworkID(pub ed25519.PublicKey) string
- func VerifyMessage(reg Registry, slug string, keyEpoch int, canonical identity.CanonicalBytes, ...) (bool, error)
- type AgentBinding
- type PublicKey
- type Registry
- type RegistrySigner
- type SignedEntry
- type SignedRegistry
Constants ¶
const ( // ErrDistMarshal wraps a distribution-envelope marshal failure. ErrDistMarshal = "registry: marshal distribution envelope" // ErrDistUnmarshal wraps a distribution-envelope unmarshal failure (bad JSON // or an unknown field — strict, fail-closed decoding). ErrDistUnmarshal = "registry: parse distribution envelope" // ErrDistMissingRegistry is returned by ParseDistribution when the registry // field is absent or empty. ErrDistMissingRegistry = "registry: distribution envelope is missing the registry field" // ErrDistMissingRootSig is returned by ParseDistribution when the root_sig // field is absent or empty. ErrDistMissingRootSig = "registry: distribution envelope is missing the root_sig field" // ErrDistBadRegistryB64 is returned when the registry field is not valid // base64-std. ErrDistBadRegistryB64 = "registry: distribution registry field is not valid base64" // ErrDistBadRootSigB64 is returned when the root_sig field is not valid // base64-std. ErrDistBadRootSigB64 = "registry: distribution root_sig field is not valid base64" // ErrDistBadSigLen is returned when the decoded root_sig is not // ed25519.SignatureSize bytes (fail closed before the verify path ever sees a // malformed signature). ErrDistBadSigLen = "registry: distribution root_sig must be ed25519.SignatureSize bytes" // ErrDistBadRootKeyEpoch is returned when root_key_epoch is negative (key // epochs are non-negative; a negative value is structurally invalid). ErrDistBadRootKeyEpoch = "registry: distribution root_key_epoch must be non-negative" // ErrDistTrailingData is returned when bytes remain in the input after the // envelope object (a trailing-data smuggling defense). ErrDistTrailingData = "registry: distribution envelope has trailing data after the JSON object" )
Distribution-envelope field names and error strings (SSOT — referenced from the (de)serialization path and asserted by tests, never inlined).
const ( // ErrPubKeyBadLen is returned by NewPublicKey for a key that is not // ed25519.PublicKeySize bytes. ErrPubKeyBadLen = "registry: public key must be ed25519.PublicKeySize bytes" // ErrPubKeySmallOrder is returned by NewPublicKey for a small-order // (universal-forgery) or undecodable public key. ErrPubKeySmallOrder = "registry: public key is small-order or undecodable" // GlobalPrefix is the reserved global-prefix slot for a network id (Door 5). // v1 ids carry this prefix so they can be made hierarchical later WITHOUT a // re-key. The id itself is derived from the keypair, never from a public git // remote (which is public + precomputable and enables traffic analysis). GlobalPrefix = "vmnet1:" )
Domain-type and namespace constants (SSOT — referenced, never inlined).
const ( // ErrRootSig is returned by VerifyAndLoad when root_sig does not verify over // the canonical registry bytes (forgery / tamper / wrong root key). ErrRootSig = "registry: root signature does not verify" // ErrRollback is returned by VerifyAndLoad when reg.epoch <= the persisted // highest-seen epoch (anti-rollback). ErrRollback = "registry: epoch is at or below the highest seen (anti-rollback)" // ErrStale is returned by VerifyAndLoad when the registry is past valid_until // or older than maxStaleness (freshness fails CLOSED). ErrStale = "registry: registry is stale or expired (fail closed)" // ErrUnknownSlug is returned by Resolve/VerifyMessage when no live binding // matches the slug. ErrUnknownSlug = "registry: no live binding for slug" // ErrRevoked is returned when a binding has revoked_at set. ErrRevoked = "registry: binding is revoked" // ErrExpiredBinding is returned when now is past a binding's valid_until. ErrExpiredBinding = "registry: binding is expired" // ErrKeyEpochMismatch is returned by VerifyMessage when the caller's // key_epoch does not match the resolved binding (default-deny). ErrKeyEpochMismatch = "registry: key_epoch does not match the live binding" // ErrBadRootKey is returned by SignRegistry for a nil/wrong-length root key. ErrBadRootKey = "registry: root private key must be ed25519.PrivateKeySize bytes" // ErrEpochRange is returned by SignRegistry/VerifyAndLoad when an epoch // (registry epoch or a binding key_epoch) is outside [1, MaxSafeEpoch]. JCS // (RFC 8785) renders numbers as IEEE-754 doubles, so an epoch above 2^53 // silently rounds (cross-language parity break + epoch confusion) and an // epoch near MaxInt64 fails JSON unmarshal (DoS); a zero/negative epoch also // breaks the monotonic anti-rollback floor. ErrEpochRange = "registry: epoch out of range [1, 2^53]" // ErrNotYetValid is returned by Resolve/VerifyMessage when now is before a // binding's valid_from (a not-yet-active binding must not resolve or verify). ErrNotYetValid = "registry: binding is not yet valid" // ErrDuplicateBinding is returned by VerifyAndLoad when the registry contains // duplicate {slug,key_epoch} tuples, or more than one live (non-revoked) // binding for the same slug (shadowing / ambiguous-resolution defense). ErrDuplicateBinding = "registry: duplicate binding (slug/key_epoch or multiple live bindings per slug)" // MaxSafeEpoch is the inclusive upper bound for any epoch (registry epoch and // binding key_epoch). It is 2^53 — the largest integer JCS (which formats // numbers as IEEE-754 doubles) can render without precision loss, so epochs in // [1, MaxSafeEpoch] round-trip identically across languages and never DoS the // JSON unmarshal path. The floor of 1 gives the monotonic counter a positive // base (negative/zero rejected). // // TODO(contract-b hardening): defense-in-depth — consider MaxSafeEpoch = (1<<53)-1. // A compromised root could hand-craft a body with epoch 2^53+1, which JCS rounds // to 2^53 (in-range at this ceiling). Unreachable via the legitimate SignRegistry // mint gate (it rejects >2^53 pre-canonicalization), so this is belt-and-suspenders // against an already-compromised root, not a real attacker capability. With the // ceiling at 2^53-1, a 2^53+1 body rounds to 2^53 which is then above-ceiling and // rejected at load too. Alternative: a round-trip re-canonicalization equality // check in VerifyAndLoad (closes the whole JCS-normalized-field-smuggling class). MaxSafeEpoch = 1 << 53 )
Trust-gate reject messages (SSOT — referenced from the verify path, asserted by callers/tests, never inlined).
const ClassAgent = "agent"
ClassAgent is the DEFAULT effective class of a binding whose Class is empty. An empty Class means "agent" so existing bindings (which set neither Class nor VouchAllowlist) authorize only the plain agent path; bridge/human authority must be explicitly GRANTED by setting Class. This is the SSOT for the default.
Variables ¶
This section is empty.
Functions ¶
func Freshness ¶ added in v0.8.0
func Freshness(env SignedRegistry) (validFrom, validUntil int64, err error)
Freshness reports a registry's validity window WITHOUT verifying the root signature.
Age and authenticity are independent questions, and conflating them cost a day of mesh silence: doctor could only answer "is this registry stale?" by asking a daemon for a copy, its daemon probe is loopback-pinned by design, and the fleet's daemon is remote — so the question was structurally unanswerable while the answer sat in a file on disk. The signature needs a root key; the clock does not.
This grants NO trust. An unverified registry claiming to be fresh proves nothing — the consuming daemon still runs the full check. It exists to PREDICT the daemon's freshness verdict locally, early enough to act.
func IsStaleAt ¶ added in v0.8.0
IsStaleAt reports whether a registry is past maxStaleness or past its valid_until at now, mirroring VerifyAndLoad's freshness rule (both bounds INCLUSIVE) so a local prediction and the daemon's verdict cannot disagree about the boundary. A future valid_from is stale too: without that guard a clock-skewed registry yields negative age and looks perpetually fresh.
func MarshalDistribution ¶
func MarshalDistribution(env SignedRegistry) ([]byte, error)
MarshalDistribution serializes a SignedRegistry to the distribution-envelope JSON, keyed by the fieldDist* SSOT constants. The registry bytes are emitted base64-std VERBATIM — they are the exact bytes root_sig covers and must not be re-canonicalized or double-encoded.
func NetworkID ¶
NetworkID derives a stable, keypair-bound network id from a public key. The id is SHA-256(pubkey) truncated and hex-encoded behind the reserved GlobalPrefix. It is derived from the AUTHENTICATOR (the keypair), never from a public git-remote fingerprint, so it rotates with a re-key and does not leak a precomputable fingerprint→slug mapping (Door 5).
func VerifyMessage ¶
func VerifyMessage( reg Registry, slug string, keyEpoch int, canonical identity.CanonicalBytes, sig []byte, now time.Time, ) (bool, error)
VerifyMessage resolves the live binding for slug, requires keyEpoch to match the binding's key_epoch (default-deny on mismatch), then verifies sig over the canonical bytes under the binding's validated pubkey via the slice-1 VerifyCanonical. A keyed slug with a revoked/expired/epoch-mismatched binding is rejected. An honest non-match returns (false, nil); a structural problem returns (false, error).
Types ¶
type AgentBinding ¶
type AgentBinding struct {
// Slug is the addressable agent label.
Slug string
// DisplayName is the human-facing name (may contain non-ASCII, e.g. "Mira ⭐").
DisplayName string
// PubKey is the validated ed25519 public key (small-order-rejected).
PubKey PublicKey
// KeyEpoch is the rotation generation of {pubkey,key_epoch}.
KeyEpoch int
// ValidFrom / ValidUntil bound the binding's validity (unix seconds).
ValidFrom int64
ValidUntil int64
// AuthorizedOriginDaemons lists the daemon ids permitted to originate for
// this slug.
AuthorizedOriginDaemons []string
// RevokedAt is the unix-seconds revocation time, or nil if the binding is
// live. A revoked binding never resolves or verifies.
RevokedAt *int
// Class is the AUTHORITY class the root GRANTS this binding (agent | bridge |
// human). An empty Class means agent (see ClassAgent / EffectiveClass). It is
// the registry's authoritative answer to a message's signed from_class CLAIM:
// the claim is authenticated by the signature, but only the binding's Class
// AUTHORIZES it. NOTE: not yet parsed from the signed registry wire schema —
// populating Class/VouchAllowlist from the canonical bytes is a separate
// cross-language slice; for now these default to empty/agent.
Class string
// VouchAllowlist is the set of principals (e.g. "human:siavoush") a bridge
// binding may vouch for. An empty allowlist authorizes NO vouch (fail closed).
VouchAllowlist []string
}
AgentBinding binds a slug to a validated ed25519 public key for one key epoch, with a freshness window and an optional revocation timestamp. The ed25519 pubkey is the identity; the slug is a label.
func Resolve ¶
Resolve returns the LIVE binding for slug at now. It default-denies: a binding with revoked_at set, one whose valid_until has passed, or one whose valid_from is still in the future (not yet active), is skipped. When the slug exists only in a revoked form, ErrRevoked is returned so the reason is distinguishable from ErrUnknownSlug; otherwise an unknown/no-live-binding slug is rejected. When multiple bindings share a slug (rotation), the first live, in-window one is returned.
func ResolveTuple ¶
ResolveTuple exposes the shared {slug,key_epoch} default-deny resolution for callers (the envelope verify layer) that need the RESOLVED binding to enforce class/vouch AUTHORITY after a successful VerifyMessage. It applies the SAME revoked/expired/not-yet-valid/epoch-mismatch default-deny as VerifyMessage, so a binding ResolveTuple returns is exactly the one VerifyMessage verified under.
func (AgentBinding) EffectiveClass ¶
func (b AgentBinding) EffectiveClass() string
EffectiveClass returns the binding's authority class, defaulting an empty Class to ClassAgent. It is the SSOT for the empty-Class-means-agent rule.
type PublicKey ¶
type PublicKey struct {
// contains filtered or unexported fields
}
PublicKey is a VALIDATED ed25519 public key: it is wrong-length-rejected and small-order-rejected AT CONSTRUCTION (NewPublicKey), so an invalid key is unrepresentable in the binding/verify path. Code that holds a PublicKey can rely on it being decodable and not a universal-forgery vector.
func NewPublicKey ¶
NewPublicKey validates b and returns a PublicKey. It rejects a wrong-length key and a small-order/undecodable key (the universal-forgery vector that stdlib ed25519.Verify leaves open) by routing through the slice-1 guard, so a hostile key cannot enter a binding. It never panics.
type Registry ¶
type Registry struct {
// Epoch is the monotonic anti-rollback counter.
Epoch int
// ValidFrom / ValidUntil bound registry freshness (unix seconds).
ValidFrom int64
ValidUntil int64
// Agents are the bindings.
Agents []AgentBinding
}
Registry is the root-signed binding set with a monotonic epoch and a freshness window.
func VerifyAndLoad ¶
func VerifyAndLoad( pinnedRootPub ed25519.PublicKey, env SignedRegistry, persistedHighestEpoch int, now time.Time, maxStaleness time.Duration, ) (Registry, int, error)
VerifyAndLoad is the load-bearing consumer trust gate. It FAILS CLOSED on every check, in order:
- ROOT SIG: verify root_sig over the canonical registry bytes via identity.VerifyCanonical (which rejects a small-order/non-canonical root key and a non-canonical signature). A bad sig is rejected.
- EPOCH RANGE: reject if reg.epoch or any binding key_epoch is outside [1, MaxSafeEpoch] (JCS-safe + anti-rollback floor).
- UNIQUENESS: reject duplicate {slug,key_epoch} tuples or more than one live binding per slug.
- ANTI-ROLLBACK: reject if reg.epoch <= persistedHighestEpoch.
- FRESHNESS: reject (fail closed) if now is before reg.valid_from, past reg.valid_until, OR (now - reg.valid_from) > maxStaleness.
Boundary convention: valid_until and maxStaleness are INCLUSIVE — a registry is honored AT exactly now == valid_until and AT exactly staleness == maxStaleness, and rejected one tick past either. valid_from is inclusive too (honored AT now == valid_from, rejected before it).
On success it returns the verified Registry and newHighestEpoch = max(persistedHighestEpoch, reg.epoch) for the caller to persist.
type RegistrySigner ¶
RegistrySigner signs JCS-canonical registry bytes via the keyless custody signer. The root private key stays in the signer process; the caller (the CLI) never loads it. The real implementation is *signer.Client; tests inject a fake to prove the sign path never opens the key file.
type SignedEntry ¶
type SignedEntry struct {
// contains filtered or unexported fields
}
SignedEntry is PROOF that a raw entry passed the Contract-B schema gate, was canonicalized, and verified under a validated PublicKey. The ONLY constructor is NewSignedEntry, which runs the full gate; a SignedEntry value therefore cannot exist for an invalid/forged entry — the invalid state is unrepresentable.
func NewSignedEntry ¶
func NewSignedEntry(pub PublicKey, rawJSON []byte, sig []byte) (SignedEntry, error)
NewSignedEntry runs the gate: it verifies sig over the SCHEMA-VALIDATED, canonicalized form of rawJSON under pub (via identity.VerifyEntry, which runs ValidateSchema + Canonicalize + cofactorless strict verify). It returns an error when the entry violates the schema, cannot be canonicalized, is structurally rejected, or simply does not verify. On success the returned SignedEntry is proof the entry is conformant and authentic.
func (SignedEntry) Canonical ¶
func (s SignedEntry) Canonical() identity.CanonicalBytes
Canonical returns the canonical bytes that were signed.
func (SignedEntry) Slug ¶
func (s SignedEntry) Slug() string
Slug returns the empty string: a SignedEntry is a transport-level proof, not a slug-bound binding. The accessor exists so callers can treat any signed payload uniformly; binding-level identity lives on AgentBinding.
type SignedRegistry ¶
type SignedRegistry struct {
// Registry is the JCS-canonical encoding of the Registry — the EXACT bytes
// that root_sig covers.
Registry []byte
// RootSig is the ed25519 signature by the offline root key over Registry.
RootSig []byte
// RootKeyEpoch identifies which root key signed (for root rotation).
RootKeyEpoch int
}
SignedRegistry is the distribution envelope: the JCS-canonical registry bytes, the ed25519 root signature over exactly those bytes, and the root key epoch.
func ParseDistribution ¶
func ParseDistribution(data []byte) (SignedRegistry, error)
ParseDistribution deserializes a distribution-envelope JSON into a SignedRegistry. It FAILS CLOSED on every malformed input — bad JSON, an unknown field, a missing/empty required field, bad base64, or a wrong-length signature — returning an error and a zero SignedRegistry (never a partial that could be mistaken for valid). It never panics.
ParseDistribution is ONLY (de)serialization + structural fail-closed parsing; it does NOT verify the root signature, freshness, or anti-rollback. The consumer flow is ParseDistribution -> VerifyAndLoad, which performs the full slice-3 trust logic over the parsed bytes.
func SignRegistry ¶
func SignRegistry(rootPriv ed25519.PrivateKey, reg Registry) (SignedRegistry, error)
SignRegistry is the OFFLINE-ROOT operation: it JCS-canonicalizes reg and ed25519-signs the canonical bytes with the root private key, returning the distribution envelope. It reuses the slice-1 SignCanonical primitive and returns an error (never panics) on a nil/wrong-length root key.
func SignRegistryWithSigner ¶
func SignRegistryWithSigner(client RegistrySigner, reg Registry) (SignedRegistry, error)
SignRegistryWithSigner is the KEYLESS parallel of SignRegistry: it runs the SAME epoch gate + JCS-canonicalization, then signs the canonical bytes via the custody signer (the root private key never enters this process) instead of a raw key. The result is byte-identical to SignRegistry over the same registry — only the signing seam differs. It FAILS CLOSED: any gate, canonicalize, or signer error returns a non-nil error and a zero SignedRegistry.