Help us shape the future of Go! Take the 2026 Go Developer Survey

registry

package
v0.10.21 Latest Latest
Warning

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

Go to latest
Published: Nov 2, 2026 License: MIT Imports: 9 Imported by: 0

README

Contract B — Registry Distribution Envelope (Slice 4)

This package implements the Contract-B trust-root registry (slice 3) and its --registry distribution envelope (slice 4): the on-disk / over-wire format a root-signed SignedRegistry is serialized to, plus the deterministic cross-language acceptance fixture that workhorse's Rust verifier binds to.

The distribution layer is (de)serialization + fail-closed parsing only. All trust logic (root-sig verify, anti-rollback, freshness, revocation, resolve, message verify) lives in registry.go (slice 3) and is not duplicated here.

Distribution envelope JSON

A distributed SignedRegistry is a single JSON object:

{
  "registry": "<base64-std of the JCS-canonical registry bytes>",
  "root_key_epoch": 0,
  "root_sig": "<base64-std of the 64-byte ed25519 root signature>"
}
  • registry — base64-std (RFC 4648, padded) of the exact JCS-canonical registry bytes that root_sig covers. These bytes are emitted verbatim; they are never re-canonicalized or double-JSON-encoded. The decoded payload is raw JCS JSON (begins with {), not a quoted string.
  • root_sig — base64-std of the ed25519 signature by the offline root key over the decoded registry bytes. Must decode to exactly ed25519.SignatureSize (64) bytes.
  • root_key_epoch — a bare JSON integer identifying which root key signed (for root rotation).

MarshalDistribution(SignedRegistry) ([]byte, error) produces this JSON; ParseDistribution([]byte) (SignedRegistry, error) parses it.

Fail-closed parsing

ParseDistribution returns an error and a zero SignedRegistry (never a partial value that could be mistaken for valid) on any malformed input:

  • non-JSON / malformed JSON,
  • an unknown/extra field (strict decoding — no silent drop of a smuggled key),
  • trailing bytes after the envelope object (concatenated-object or garbage defense),
  • a missing or empty registry or root_sig,
  • a registry or root_sig that is not valid base64-std,
  • a root_sig that does not decode to 64 bytes,
  • a negative root_key_epoch (key epochs are non-negative; a negative value is structurally invalid).

It never panics. It does not verify the root signature, freshness, or anti-rollback — those are VerifyAndLoad's job.

JCS canonical registry / binding shape

The registry bytes inside the envelope are the RFC 8785 (JCS) canonical form of:

{
  "agents": [
    {
      "authorized_origin_daemons": ["daemon-1"],
      "display_name": "Mira ⭐",     // raw UTF-8, never \u-escaped
      "key_epoch": 1,                // bare integer
      "pubkey": "<base64-std 32-byte ed25519 pubkey>",
      "revoked_at": 1700000000,      // OMITTED when the binding is live
      "slug": "mira",
      "valid_from": 1990000,         // unix seconds, bare integer
      "valid_until": 3000000
    }
  ],
  "epoch": 10,                       // monotonic anti-rollback counter, bare int
  "valid_from": 1990000,
  "valid_until": 3000000
}

Canonicalization rules (enforced by identity.Canonicalize):

  • object keys sorted lexicographically by UTF-16 code unit,
  • field names snake_case,
  • pubkeys base64-std,
  • integers rendered bare (no decimal point / exponent); epochs are bounded to [1, 2^53] so they round-trip across languages without IEEE-754 precision loss,
  • revoked_at omitted when the binding is live (nil pointer == live), present only when revoked,
  • string values preserved as raw UTF-8 (e.g. ⭐ stays 0xE2 0xAD 0x90).

Verify order (consumer / Rust verifier contract)

ParseDistribution then registry.VerifyAndLoad perform, fail-closed at every step, in this order:

  1. Parse the envelope (fail-closed on malformed input, above).
  2. Root signature first, over the received registry bytes — verify root_sig against the pinned root pubkey using ZIP-215 strict verification with an explicit small-order pubkey reject (cofactor check). A tampered body or a small-order root key is rejected before the body is trusted.
  3. Decode the now-authenticated bytes; every agent pubkey is re-validated (wrong-length / small-order / undecodable agent keys are rejected at load).
  4. Epoch range — reject a registry epoch or any binding key_epoch outside [1, 2^53].
  5. Uniqueness — reject duplicate {slug, key_epoch} tuples or more than one live binding per slug (shadowing defense).
  6. Anti-rollback — reject epoch <= persistedHighestEpoch.
  7. Freshness (fail closed) — reject if now < valid_from (reject-future-valid_from), now > valid_until, or now - valid_from > maxStaleness. A stale registry may hide a revocation.

Then, per message:

  1. Resolve the live binding for the slug at now, default-denying a revoked, expired, or not-yet-valid (now < binding.valid_from) binding.
  2. VerifyMessage — require the caller's key_epoch to match the resolved binding (default-deny on mismatch), then verify the message signature under the binding's pubkey with the same ZIP-215 strict + small-order + non-canonical-S rejection.

Cross-language acceptance fixture

testdata/cross_language_vectors.json is the gating acceptance test and the stable artifact workhorse's Rust verifier binds to. It is generated deterministically (fixed ed25519 seeds) by TestGenerate_CrossLanguageFixture (run go test ./internal/identity/registry/ -run TestGenerate_CrossLanguageFixture -update) and committed.

Top-level shape:

  • root_pubkey — base64-std of the pinned root pubkey to verify against.
  • now, max_staleness_secs — the reference time and staleness bound the negatives are relative to.
  • valid — a complete --registry envelope that loads under root_pubkey/now/max_staleness_secs, plus the expected resolved binding (expect_slug / expect_key_epoch / expect_pubkey) and a sample {slug, key_epoch, canonical_message_bytes, signature} that VerifyMessage accepts.
  • reject — named negative cases, each {name, reason, expect:"reject", envelope, ...} with optional root_pubkey / now / persisted_highest_epoch overrides and an optional message sample. Cases cover: small-order root pubkey, small-order agent pubkey, non-canonical S in the message signature, future valid_from (registry-level and binding-level), rollback (epoch <= persisted), stale (now > valid_until and beyond max_staleness), revoked binding, and a tampered registry body.

The Go side proves the fixture is correct before Rust consumes it: TestCrossLanguageFixture_ValidLoadsAndVerifies asserts the valid case loads + resolves + verifies, and TestCrossLanguageFixture_AllRejectsAreRejected asserts every reject case is rejected somewhere in ParseDistribution / VerifyAndLoad / Resolve / VerifyMessage. The Rust verify_strict + cofactor + freshness + anti-rollback path must reject the same cases and accept the same valid case.

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

View Source
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).

View Source
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).

View Source
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).

View Source
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

func IsStaleAt(validFrom, validUntil int64, now time.Time, maxStaleness time.Duration) bool

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

func NetworkID(pub ed25519.PublicKey) string

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

func Resolve(reg Registry, slug string, now time.Time) (AgentBinding, error)

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

func ResolveTuple(reg Registry, slug string, keyEpoch int, now time.Time) (AgentBinding, error)

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

func NewPublicKey(b []byte) (PublicKey, error)

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.

func (PublicKey) Bytes

func (p PublicKey) Bytes() ed25519.PublicKey

Bytes returns a defensive COPY of the raw 32-byte public key. A copy (not an alias to internal storage) preserves the validated-key invariant: a caller that mutates the returned slice cannot corrupt the PublicKey held in a binding.

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:

  1. 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.
  2. EPOCH RANGE: reject if reg.epoch or any binding key_epoch is outside [1, MaxSafeEpoch] (JCS-safe + anti-rollback floor).
  3. UNIQUENESS: reject duplicate {slug,key_epoch} tuples or more than one live binding per slug.
  4. ANTI-ROLLBACK: reject if reg.epoch <= persistedHighestEpoch.
  5. 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

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

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.

Jump to

Keyboard shortcuts

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