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

updates

package
v0.11.0 Latest Latest
Warning

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

Go to latest
Published: Oct 25, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

README

Agent update contracts — offline verification foundation

updates.Verify and workspace-agent's kw_management::updates::verify verify the same publisher-signed manifest contract. These libraries implement no download, installation, grant issuance, listener, availability endpoint or live upgrade capability. A valid signature is artifact authenticity, not permission to execute an updater. Production publisher-key custody and management/job/ recording integration remain separate prerequisites.

Trust and byte framing

Callers supply an independently configured publisher public-key ring (1–16 Ed25519 public keys), key ID, expected manifest SHA-256 and detached signature. The manifest cannot supply trust roots, expected digest or a download URL.

  • Key ID: first 16 bytes of SHA-256(public key), lowercase hex (32 characters).
  • Manifest digest: SHA-256 of the exact raw JSON bytes, lowercase hex.
  • Signature: Ed25519 over ASCII kw-agent-update-manifest/v1, one NUL byte, then those raw manifest bytes. Do not reserialize before verifying. Whitespace changes require a different signature and independently approved digest.
  • Signature/public-key byte inputs are raw 64/32-byte values. The shared fixture metadata encodes them as canonical, unpadded base64url for test transport.

The signed test fixtures are pinned to LF by .gitattributes in both repos; Windows checkout must not rewrite their bytes. Runtime verification never normalizes line endings or whitespace to make a changed signature pass.

Use a dedicated publisher key, independent of API grant and management CA keys. Rotation is an explicit operator keyring update; retiring a key denies manifests signed by it. There is no key fetch, algorithm negotiation, unsigned mode, trust-on-first-use or implicit operating-system trust fallback.

JSON schema and bounds

testdata/manifest-v1.json shows the complete wire schema, including optional IDD metadata. Required objects/fields must be present and non-null; driver may be omitted or null. rollback_predecessors must be an array (empty is allowed), never omitted/null. Field names are exact and case-sensitive. Both implementations reject unknown/duplicate fields, including escaped duplicate aliases, invalid UTF-8, trailing JSON and noninteger numeric fields.

  • Manifest: at most 64 KiB; version exactly 1; OS windows, architecture amd64.
  • Target IDs and artifact filenames: 1–128 ASCII lowercase letters/digits plus ., _, -, beginning with a letter/digit. Filenames are basenames, reject trailing dots and Windows reserved device stems (con, nul, com1, …). Paths, drive prefixes, alternate streams and URLs are not accepted.
  • Artifact sizes: 1 byte–2 GiB, hashes exactly 64 lowercase hex characters. MSI/updater/IDD package suffixes are .msi, .exe, .zip respectively.
  • Agent versions and compatibility bounds: canonical numeric major.minor.patch, no prefix/prerelease/leading zero; major/minor ≤255, patch ≤65535 (MSI bounds). Minimum installed version must not exceed maximum. Driver version uses four canonical numeric uint16 components.
  • MSI ProductCode: canonical lowercase nonzero GUID without braces. UpgradeCode is fixed to the agent's stable f72bb43e-dba8-4dbb-a963-b201e6c0d885, distinct from ProductCode. The eventual installer must check actual registration too.
  • Updater protocol is 1. Management/desktop compatibility ranges are positive, ordered and bounded to 64. These are declarations, not observed capabilities.
  • Optional IDD requires package identity/hash, signing-certificate SHA-256 and 1–8 exact, case-insensitively unique ASCII hardware IDs (≤128 bytes, letters, digits, _, ., &, -, \, with a \). No wildcard device selection.
  • At most 16 rollback predecessors: unique target IDs and manifest digests, no self-target. Reboot policy: never or may-require; actual reboot still requires separate administrator consent and installed-device evidence.

The verifier does not prove Windows package signatures, archive contents, IDD qualification, guest version compatibility, target allowlisting/revocation or rollback safety. Those checks belong to the eventual availability/admission and independent updater implementation. Publisher-signed metadata alone never makes an arbitrary driver safe to install.

Payload verification

A verified manifest retains its own immutable artifact identities. Go metadata access returns a deep copy; Rust returns a shared immutable reference. VerifyArtifact/verify_artifact selects an exact signed filename and streams at most its signed size plus one byte into SHA-256. It rejects missing, oversized, truncated, corrupt and unknown payloads, including I/O errors after the expected bytes. It does not buffer an entire MSI or open paths/URLs.

Test vectors

The manifest and signature JSON are byte-identical to workspace-agent/kw-management/tests/testdata/manifest-v1*.json. Tests on both sides verify publisher signature/digest and the streams msi-v1, helper-v1, idd-v1. These are synthetic bytes, not installers or a release. The fixture key is deterministically derived from the public test seed [0x55; 32]; it must never be deployed as publisher trust.

Regression tests re-sign malformed schemas with that synthetic key so validation is exercised independently from signature failure. They cover substitution, domain confusion, duplicate/unknown/case-confused fields, Windows path/device names, numeric bounds, rollback ambiguity, payload loss/flood and mutable-copy isolation. Go additionally supplies a signed-parser fuzz target.

Authenticated machine inventory

VerifyInventory and Rust kw_management::updates::inventory::verify authenticate management.inventory.v1. The exact payload bytes are signed under ASCII kw-management-inventory/v1 plus one NUL with a pinned agent identity keyring, then bound to the independently minted 256-bit nonce and a 60-second freshness window with no future skew. The verified payload SHA-256 is the inventory digest later pinned as expected_installed_inventory_sha256.

The schema (testdata/inventory-v1.json) carries namespace/name, workspace and VM UIDs, revocation epoch, OS/architecture, inventory timestamp/nonce, agent version and build digest, supported management/updater protocol ranges, registered MSI ProductCode/UpgradeCode/version/install-directory entries (exactly one agent UpgradeCode registration, at most eight), the optional IDD package/INF/device/driver-version identity, pending-reboot state and the optional updater journal/active-attempt observation. Every string field is ASCII-bounded and both verifiers require a byte-identical canonical re-encode, so Go and Rust accept exactly the same byte set; an absent driver or attempt section must appear as explicit JSON null.

Inventory is signed evidence, never upgrade authority: availability comparison, start-time re-read and admission remain separate. The fixture bytes are byte-identical to workspace-agent's tests/testdata/inventory-v1*.json and are signed by the synthetic test seed [0x5A; 32], never a deployed identity.

Documentation

Overview

Package updates verifies publisher-authenticated agent-update manifests, authenticated machine inventory reports and the write-ahead upgrade journal, plus bounded payload streams. It grants no permission to install, downloads nothing and does not implement the updater, management transport or job lifecycle.

Index

Constants

View Source
const (
	InventoryDomain        = "kw-management-inventory/v1\x00"
	MaxInventoryBytes      = 32 * 1024
	InventoryFreshSeconds  = 60
	MaxInventoryMSIEntries = 8
)

InventoryDomain frames authenticated management.inventory.v1 reports. The signature covers the domain plus the exact payload bytes; freshness and the server-minted nonce binding are checked after authenticity.

View Source
const (
	MaxJournalBytes          = 16 * 1024
	MaxJournalSequence       = 1_000_000
	AttemptSeconds     int64 = 15 * 60
	RebootWaitSeconds  int64 = 24 * 60 * 60
)
View Source
const (
	Domain                  = "kw-agent-update-manifest/v1\x00"
	MaxManifestBytes        = 64 * 1024
	MaxArtifactBytes uint64 = 2 * 1024 * 1024 * 1024
	AgentUpgradeCode        = "f72bb43e-dba8-4dbb-a963-b201e6c0d885"
)

Variables

View Source
var (
	ErrInventory      = errors.New("invalid machine inventory")
	ErrInventoryFresh = errors.New("machine inventory is stale, future-dated or bound to a different nonce")
)
View Source
var (
	ErrJournal     = errors.New("invalid update journal")
	ErrFence       = errors.New("update journal binding or owner fence changed")
	ErrTransition  = errors.New("update journal transition denied")
	ErrClock       = errors.New("update journal clock is stale or out of bounds")
	ErrRecording   = errors.New("update journal recording acknowledgment required")
	ErrPersistence = errors.New("update journal persistence outcome unavailable; reload before continuing")
	ErrConflict    = errors.New("update journal compare-and-swap conflict")
)
View Source
var (
	ErrInvalid   = errors.New("invalid agent update manifest")
	ErrUntrusted = errors.New("agent update publisher is not trusted")
	ErrDigest    = errors.New("agent update manifest digest mismatch")
	ErrSignature = errors.New("agent update manifest signature mismatch")
	ErrPayload   = errors.New("agent update payload is incomplete, oversized or corrupt")
)

Functions

func Digest

func Digest(raw []byte) string

func KeyID

func KeyID(key ed25519.PublicKey) string

Types

type ActionReceipt

type ActionReceipt struct {
	Sequence         uint64 `json:"sequence"`
	Kind             string `json:"kind"`
	Outcome          string `json:"outcome"`
	MSICode          *int64 `json:"msi_code"`
	ServiceState     string `json:"service_state"`
	ProductCode      string `json:"product_code"`
	TargetAbsent     bool   `json:"target_absent"`
	DriverSHA256     string `json:"driver_sha256"`
	IdentityVerified bool   `json:"identity_verified"`
	HealthVerified   bool   `json:"health_verified"`
}

type Artifact

type Artifact struct {
	File   string `json:"file"`
	SHA256 string `json:"sha256"`
	Size   uint64 `json:"size"`
}

type Compatibility

type Compatibility struct {
	MinimumAgentVersion string `json:"minimum_agent_version"`
	MaximumAgentVersion string `json:"maximum_agent_version"`
	ManagementMin       uint16 `json:"management_min"`
	ManagementMax       uint16 `json:"management_max"`
	DesktopMin          uint16 `json:"desktop_min"`
	DesktopMax          uint16 `json:"desktop_max"`
}

type Driver

type Driver struct {
	Artifact     Artifact `json:"artifact"`
	Version      string   `json:"version"`
	SignerSHA256 string   `json:"signer_sha256"`
	HardwareIDs  []string `json:"hardware_ids"`
}

type Inventory

type Inventory struct {
	Version         uint32            `json:"version"`
	Nonce           string            `json:"nonce"`
	Timestamp       int64             `json:"timestamp"`
	Namespace       string            `json:"namespace"`
	Name            string            `json:"name"`
	WorkspaceUID    string            `json:"workspace_uid"`
	VMUID           string            `json:"vm_uid"`
	RevocationEpoch uint64            `json:"revocation_epoch"`
	OS              string            `json:"os"`
	Architecture    string            `json:"architecture"`
	AgentVersion    string            `json:"agent_version"`
	BuildSHA256     string            `json:"build_sha256"`
	ManagementMin   uint16            `json:"management_min"`
	ManagementMax   uint16            `json:"management_max"`
	UpdaterMin      uint16            `json:"updater_min"`
	UpdaterMax      uint16            `json:"updater_max"`
	MSI             []InventoryMSI    `json:"msi"`
	Driver          *InventoryDriver  `json:"driver"`
	PendingReboot   bool              `json:"pending_reboot"`
	Attempt         *InventoryAttempt `json:"attempt"`
}

Inventory is the authenticated machine report. The exact bytes are digested into expected_installed_inventory_sha256 at start; a report is evidence, not upgrade authority.

func (Inventory) Encode

func (i Inventory) Encode() []byte

Encode reproduces the exact signed bytes. SetEscapeHTML(false) keeps the output byte-identical to serde_json for the ASCII-only schema, including install directories containing '&'.

type InventoryAttempt

type InventoryAttempt struct {
	AttemptID string `json:"attempt_id"`
	Phase     string `json:"phase"`
	Sequence  uint64 `json:"sequence"`
}

type InventoryDriver

type InventoryDriver struct {
	PackageIdentity string `json:"package_identity"`
	PackageSHA256   string `json:"package_sha256"`
	InfName         string `json:"inf_name"`
	DeviceInstance  string `json:"device_instance"`
	DriverVersion   string `json:"driver_version"`
}

type InventoryMSI

type InventoryMSI struct {
	ProductCode      string `json:"product_code"`
	UpgradeCode      string `json:"upgrade_code"`
	Version          string `json:"version"`
	InstallDirectory string `json:"install_directory"`
}

type Journal

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

Journal is immutable between successfully persisted transitions.

func DecodeJournal

func DecodeJournal(raw []byte, planDigest, checkpointDigest string, minimumSequence uint64) (*Journal, error)

DecodeJournal needs the admitted plan digest and a trusted checkpoint digest/ sequence floor. A self-reported hash is corruption detection, not authority.

func NewJournal

func NewJournal(plan JournalPlan, owner uint64, now int64) (*Journal, error)

func (*Journal) Encode

func (j *Journal) Encode() []byte

func (*Journal) Prepare

func (j *Journal) Prepare(event JournalEvent, c JournalContext) (*JournalTransition, error)

func (*Journal) Recovery

func (j *Journal) Recovery(c JournalContext) string

Recovery is advisory only. Active actions are observed/reconciled, never replayed; callers still need fresh native inventory and authenticated permits.

func (*Journal) SHA256

func (j *Journal) SHA256() string

func (*Journal) Snapshot

func (j *Journal) Snapshot() JournalSnapshot

type JournalAck

type JournalAck struct {
	Sequence uint64
	SHA256   string
	Kind     string
}

type JournalAction

type JournalAction struct {
	Sequence uint64 `json:"sequence"`
	Kind     string `json:"kind"`
	BootID   string `json:"boot_id"`
}

type JournalBinding

type JournalBinding struct {
	AttemptID       string `json:"attempt_id"`
	Namespace       string `json:"namespace"`
	WorkspaceName   string `json:"workspace_name"`
	WorkspaceUID    string `json:"workspace_uid"`
	VMUID           string `json:"vm_uid"`
	RevocationEpoch uint64 `json:"revocation_epoch"`
	ActorUID        string `json:"actor_uid"`
	RecordingID     string `json:"recording_id"`
	TargetSHA256    string `json:"target_sha256"`
	PreviousSHA256  string `json:"previous_sha256"`
	InventorySHA256 string `json:"inventory_sha256"`
}

JournalBinding must be supplied by authenticated admission/current identity, never reconstructed as authority from a guest journal or client annotation.

type JournalContext

type JournalContext struct {
	Binding            JournalBinding
	OwnerFence         uint64
	Now                int64
	BootID             string
	Authorized         bool
	RecordingAvailable bool
	InstallerBusy      bool
}

type JournalEvent

type JournalEvent struct {
	Kind    string // begin, complete, cancel, rollback, observe_boot, claim, manual, recording_lost, finalize
	Receipt *ActionReceipt
}

type JournalPlan

type JournalPlan struct {
	Binding              JournalBinding `json:"binding"`
	TargetProductCode    string         `json:"target_product_code"`
	PreviousProductCode  string         `json:"previous_product_code"`
	TargetDriverSHA256   string         `json:"target_driver_sha256"`
	PreviousDriverSHA256 string         `json:"previous_driver_sha256"`
	AllowReboot          bool           `json:"allow_reboot"`
}

JournalPlan is a projection of verified manifests and authenticated inventory. Its validation is structural, not publisher trust or installation permission.

func (JournalPlan) SHA256

func (p JournalPlan) SHA256() string

type JournalSnapshot

type JournalSnapshot struct {
	Version             uint32         `json:"version"`
	Plan                JournalPlan    `json:"plan"`
	Sequence            uint64         `json:"sequence"`
	OwnerFence          uint64         `json:"owner_fence"`
	Phase               string         `json:"phase"`
	Flow                string         `json:"flow"`
	Step                uint32         `json:"step"`
	Active              *JournalAction `json:"active"`
	LastReceipt         *ActionReceipt `json:"last_receipt"`
	Mutated             bool           `json:"mutated"`
	ServiceNeedsRestore bool           `json:"service_needs_restore"`
	CancelRequested     bool           `json:"cancel_requested"`
	RebootCount         uint32         `json:"reboot_count"`
	WaitBootID          string         `json:"wait_boot_id"`
	WaitingSince        int64          `json:"waiting_since"`
	StartedAt           int64          `json:"started_at"`
	UpdatedAt           int64          `json:"updated_at"`
	ExpiresAt           int64          `json:"expires_at"`
	CleanupResult       string         `json:"cleanup_result"`
	Result              string         `json:"result"`
}

type JournalStore

type JournalStore interface {
	CompareAndSwap(expected string, next []byte) error
}

JournalStore must durably and atomically replace the checkpoint only if its current digest matches expected. Uncertain writes return an error, even if they might have committed. A native ACL/flush/lock adapter is still required.

type JournalTransition

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

func (*JournalTransition) Bytes

func (t *JournalTransition) Bytes() []byte

func (*JournalTransition) Commit

Commit returns an action descriptor only after durable CAS and matching off-guest intent acknowledgment. Descriptors are not signed execution grants. Any lost/uncertain persistence reply returns no action: reload/reconcile.

func (*JournalTransition) SHA256

func (t *JournalTransition) SHA256() string

func (*JournalTransition) Sequence

func (t *JournalTransition) Sequence() uint64

type MSI

type MSI struct {
	Artifact    Artifact `json:"artifact"`
	ProductCode string   `json:"product_code"`
	UpgradeCode string   `json:"upgrade_code"`
}

type Manifest

type Manifest struct {
	Version              uint32        `json:"version"`
	TargetID             string        `json:"target_id"`
	OS                   string        `json:"os"`
	Architecture         string        `json:"architecture"`
	AgentVersion         string        `json:"agent_version"`
	BuildSHA256          string        `json:"build_sha256"`
	MSI                  MSI           `json:"msi"`
	Updater              Updater       `json:"updater"`
	Driver               *Driver       `json:"driver,omitempty"`
	Compatibility        Compatibility `json:"compatibility"`
	RollbackPredecessors []Predecessor `json:"rollback_predecessors"`
	RebootPolicy         string        `json:"reboot_policy"`
}

type Predecessor

type Predecessor struct {
	TargetID       string `json:"target_id"`
	ManifestSHA256 string `json:"manifest_sha256"`
}

type Updater

type Updater struct {
	Artifact        Artifact `json:"artifact"`
	ProtocolVersion uint16   `json:"protocol_version"`
}

type VerifiedInventory

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

func VerifyInventory

func VerifyInventory(raw, signature []byte, keyID, expectedNonce string, now int64, keys map[string]ed25519.PublicKey) (*VerifiedInventory, error)

VerifyInventory requires the independently minted nonce and current time in addition to a pinned agent identity keyring. Every string field is ASCII-bounded so the canonical re-encode matches serde_json byte for byte.

func (*VerifiedInventory) Metadata

func (v *VerifiedInventory) Metadata() Inventory

Metadata returns a deep copy so availability callers cannot mutate the verified report behind the digest.

func (*VerifiedInventory) SHA256

func (v *VerifiedInventory) SHA256() string

type VerifiedManifest

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

VerifiedManifest keeps its verified payload identities private. Metadata() returns a deep copy so display/availability callers cannot replace the hashes used by VerifyArtifact after publisher verification.

func Verify

func Verify(raw, signature []byte, keyID, expectedDigest string, keys map[string]ed25519.PublicKey) (*VerifiedManifest, error)

Verify requires an operator-pinned manifest digest and independently supplied publisher keyring. No trust key or download URL comes from the signed body. Signatures cover the domain plus the exact raw bytes, including whitespace.

func (*VerifiedManifest) Metadata

func (v *VerifiedManifest) Metadata() Manifest

func (*VerifiedManifest) SHA256

func (v *VerifiedManifest) SHA256() string

func (*VerifiedManifest) VerifyArtifact

func (v *VerifiedManifest) VerifyArtifact(name string, reader io.Reader) error

VerifyArtifact reads at most the signed size plus one byte. It never buffers an entire installer, accepts trailing content, or trusts a caller's size/hash.

Jump to

Keyboard shortcuts

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