Documentation
¶
Overview ¶
Package selfupdate replaces the running binary with a newer signed release.
It is the framework's public package over framework/internal/service/selfupdate (ADR 0158): the value types and ports are aliases of the core selfupdate types and the functions delegate straight to the service implementation, which is built on pkg/v1 and nothing below it.
The order is the security property ¶
The hard part of a self-update is not the download. It is the order in which a candidate becomes trusted, and this package fixes it:
- a detached ed25519 signature over the checksum manifest, verified against the vendor key linked into the build;
- the archive's SHA-256 against that now-authenticated manifest;
- only then does anything touch the disk.
A digest checked against an unauthenticated manifest proves nothing — an attacker who can substitute the archive can substitute the manifest beside it. Reversing steps 1 and 2 therefore turns the whole chain into decoration, which is why the order is asserted by the suite rather than left to a comment.
A build with no vendor key installs NOTHING. That direction is deliberate: the alternative, skipping verification when no key is present, makes the security property depend on a build flag nobody checks.
Usage ¶
src := selfupdate.Source{
Owner: "acme",
StableRepo: "widget",
Product: "widget",
}
svc := selfupdate.New(version, src).WithVendorKey(vendorKey)
info, err := svc.CheckForUpdate()
if err != nil { /* errs.HasCode(err, selfupdate.CodeDevBuild) … */ }
if info.Available {
_, err = svc.Upgrade()
}
Consent is separate from the upgrade, and escalation is separate again ¶
An explicit upgrade command is a deliberate act and needs no permission. An upgrade a caller did NOT ask for — a version floor enforced mid-run — does, and Source.AuthoriseUnattendedUpgrade is where that is decided: an explicit environment value settles it in either direction, otherwise a human is asked, otherwise the answer is no. Silence is not permission.
Replacing a binary in a directory the user cannot write needs a SECOND opt-in. The two are deliberately different variables: opting into unattended upgrades must not silently grant privilege escalation.
Both variable names are derived from Source.Product — `widget` yields WIDGET_AUTO_UPGRADE and WIDGET_ALLOW_SUDO — by uppercasing and folding punctuation to underscore.
A product whose updates are silent by design builds its Service WithAutomaticConsent — its own consent, given at build, which Service.AuthoriseUnattendedUpgrade reads and an operator still overrules with <PREFIX>_AUTO_UPGRADE=0. It grants no escalation, and WithoutElevation forbids escalation outright, whatever <PREFIX>_ALLOW_SUDO says (ADR 0150).
Keys that rotate, a signature that names its release, a probe that rolls back ¶
Service.WithVendorKeys links several keys, in order, and a release verifies against any of them: a rotation publishes under the new key while builds that carry both accept it, and neither side has to be updated first (ADR 0150). Service.WithSignatureDomain makes each signature cover a domain — so a key that also signs other documents cannot have one read as a release — and makes the signed manifest say which tag it is and until when it may be installed ("# tag v1.4.0", "# expires 2026-12-31T00:00:00Z"): an older release replayed under a newer name, or a stale one, is refused. Service.WithProbe keeps the previous binary as <binary>.prev and runs the new one with the product's probe arguments; a probe that fails puts the previous one back (CodeProbeFailed).
Companions: a second executable from the same archive ¶
A product that ships more than one executable in its release archive — a thin client beside a daemon — declares the others with Service.WithCompanions. Each one installed beside the running binary is replaced from the archive that was already authenticated, as one set with the binary: every file staged, every previous one kept, then renamed, and a rename or a probe that fails puts back the files already replaced. One that is not installed is not created.
Two limits a caller must know before relying on this ¶
**Without a signature domain, a compromised release host can serve an OLDER signed release.** The historical manifest is signed but carries no release tag, so a host that answers a request for v2 with v1's manifest, signature and archive passes every check and installs v1. WithSignatureDomain closes it by putting the tag and an expiry INSIDE the signed document; a product that has not adopted the format keeps the gap.
**Windows is not supported for the replacement step.** Windows will not let a running executable be renamed over, so on a Windows build Upgrade refuses with the SDK's UNSUPPORTED_PLATFORM — errors.Is(err, proc.UnsupportedPlatform) — before it downloads anything. CheckForUpdate works there: a product can still tell its user that a newer release exists. A build with no vendor key is told about the key first, on every platform.
Both are recorded in ADR 0077 §Deferred rather than left to be discovered, and the Windows refusal in ADR 0095.
What this package does not do ¶
It does not roll back on its own. The replacement is atomic (temp file, chmod, rename) so there is no window where the binary is half-written, and without WithProbe the previous version is gone once the rename lands; with a probe it is kept as <binary>.prev and put back when the probe fails.
Index ¶
Constants ¶
const CandidateListSentinel string = svcupd.CandidateListSentinel
CandidateListSentinel is the tag value meaning "list the candidates rather than install one".
It exists because a CLI flag taking an optional value has to spell "the flag was given without a value" AS a value. A caller wires it into its own flag definition, so the sentinel has to be reachable from outside the package that consumes it.
const CodeAPIBodyTooLarge errs.Code = coreupd.CodeAPIBodyTooLarge
CodeAPIBodyTooLarge identifies release metadata over its read cap.
const CodeArchiveTooLarge errs.Code = coreupd.CodeArchiveTooLarge
CodeArchiveTooLarge identifies a release archive over the in-memory cap.
const CodeArchiveUnreadable errs.Code = svcupd.CodeArchiveUnreadable
CodeArchiveUnreadable identifies a release archive that would not unpack. It is raised only AFTER the signature and digest have both verified, so it is a packaging or transfer fault and never a supply-chain signal.
const CodeBinaryNotInArchive errs.Code = coreupd.CodeBinaryNotInArchive
CodeBinaryNotInArchive identifies a verified archive missing the binary.
const CodeCandidateNotFound errs.Code = coreupd.CodeCandidateNotFound
CodeCandidateNotFound identifies a requested candidate that does not exist.
const CodeCandidateTagRequired errs.Code = svcupd.CodeCandidateTagRequired
CodeCandidateTagRequired identifies a candidate install asked for without naming which candidate. It wraps errors.ErrUnsupported, so a caller matching that instead still matches.
const CodeChecksumMismatch errs.Code = coreupd.CodeChecksumMismatch
CodeChecksumMismatch identifies an archive whose digest did not match its authenticated manifest entry.
const CodeChecksumMissing errs.Code = coreupd.CodeChecksumMissing
CodeChecksumMissing identifies an archive with no entry in the authenticated manifest.
const CodeDevBuild errs.Code = coreupd.CodeDevBuild
CodeDevBuild identifies a check on a build with no release version.
const CodeDownloadFailed errs.Code = coreupd.CodeDownloadFailed
CodeDownloadFailed identifies a download that did not complete. It is the transient one: a retry is reasonable.
const CodeDraftRelease errs.Code = coreupd.CodeDraftRelease
CodeDraftRelease identifies a draft release, which exposes no asset.
const CodeElevationFailed errs.Code = svcupd.CodeElevationFailed
CodeElevationFailed identifies an escalation the operator DID authorise and that the system refused anyway. Unlike CodeElevationNotAuthorised, no environment variable repairs it.
const CodeElevationNotAuthorised errs.Code = coreupd.CodeElevationNotAuthorised
CodeElevationNotAuthorised identifies a replacement needing privilege the operator did not opt into. Recoverable, but only through the opt-in.
const CodeExecutablePathUnresolved errs.Code = svcupd.CodeExecutablePathUnresolved
CodeExecutablePathUnresolved identifies an update that cannot name the file it would replace. Nothing is attempted after it.
const CodeInsecureRedirect errs.Code = coreupd.CodeInsecureRedirect
CodeInsecureRedirect identifies a redirect leaving HTTPS or over the hop bound.
const CodeInvalidTag errs.Code = coreupd.CodeInvalidTag
CodeInvalidTag identifies a tag that is not a valid semantic version.
const CodeNoVendorKey errs.Code = coreupd.CodeNoVendorKey
CodeNoVendorKey identifies an install attempted by a build carrying no vendor key. Match it with errs.HasCode: it is the one refusal a retry never fixes.
const CodeNotPrerelease errs.Code = coreupd.CodeNotPrerelease
CodeNotPrerelease identifies a tag requested as a candidate that is stable.
const CodeProbeFailed errs.Code = svcupd.CodeProbeFailed
CodeProbeFailed identifies a replacement whose new binary did not answer the probe the product declared with Service.WithProbe; the previous binary is put back from <binary>.prev when it was kept (ADR 0150).
const CodeReleaseMetadataUnreadable errs.Code = svcupd.CodeReleaseMetadataUnreadable
CodeReleaseMetadataUnreadable identifies a release-API answer that arrived whole and did not decode. The bytes came, so no transport retry applies.
const CodeReplacementFailed errs.Code = svcupd.CodeReplacementFailed
CodeReplacementFailed identifies a rename over the running executable that did not land.
const CodeSignatureInvalid errs.Code = coreupd.CodeSignatureInvalid
CodeSignatureInvalid identifies a manifest signature that did not verify. With CodeSignatureMissing it is the supply-chain pair: no retry helps, and no manual install is safe.
const CodeSignatureMissing errs.Code = coreupd.CodeSignatureMissing
CodeSignatureMissing identifies a release whose manifest signature was absent.
const CodeStagingFailed errs.Code = svcupd.CodeStagingFailed
CodeStagingFailed identifies a replacement that failed BEFORE the rename, so the running binary is byte-for-byte as it was. That is the whole difference between this and CodeReplacementFailed, and it is the fact an operator needs.
const CodeUnexpectedStatus errs.Code = coreupd.CodeUnexpectedStatus
CodeUnexpectedStatus identifies an unusable status from the release host.
const CodeUnknownArchive errs.Code = coreupd.CodeUnknownArchive
CodeUnknownArchive identifies an asset in an unhandled container format.
Variables ¶
var ( // NoVendorKey is returned when the running build carries no vendor key, // so nothing could have authenticated the release. A REFUSAL, not a // degradation: there is no weaker check to fall back to. NoVendorKey = coreupd.NoVendorKey // SignatureMissing is returned when the detached signature over the // checksum manifest could not be fetched. An unsigned release is never // installed and no environment variable reopens that. SignatureMissing = coreupd.SignatureMissing // SignatureInvalid is returned when the manifest's signature does not // verify against the vendor key. With SignatureMissing it is the // supply-chain pair: no retry helps and no manual install is safe. SignatureInvalid = coreupd.SignatureInvalid // ChecksumMissing is returned when the authenticated manifest has no entry // for the archive being installed. ChecksumMissing = coreupd.ChecksumMissing // ChecksumMismatch is returned when the archive's digest does not match // its authenticated manifest entry — a corrupted download, or a release // whose assets changed after it was signed. ChecksumMismatch = coreupd.ChecksumMismatch // ArchiveTooLarge is returned when a release archive exceeds the size cap, // which exists because the bytes are buffered before anything has vouched // for them. It is also what an oversized checksums.txt raises, deliberately // — a truncated manifest fails signature verification, and reporting a size // problem as a supply-chain one is the worst advice this package can give. ArchiveTooLarge = coreupd.ArchiveTooLarge // APIBodyTooLarge is returned when a release-metadata response exceeds its // read cap. APIBodyTooLarge = coreupd.APIBodyTooLarge // InsecureRedirect is returned when a redirect leaves HTTPS or exceeds the // hop bound. Following it would hand the archive fetch to a plaintext hop. InsecureRedirect = coreupd.InsecureRedirect // UnexpectedStatus is returned when the release host answers with a status // the update path does not accept. Retryable. UnexpectedStatus = coreupd.UnexpectedStatus // DownloadFailed is returned when a download does not complete, including // a transport failure with no HTTP status at all. It is the sentinel a // retry policy keys on. DownloadFailed = coreupd.DownloadFailed // DevBuild is returned when a version check runs on a build carrying no // release version, so there is nothing to compare a release against. It is // an ANSWER rather than a fault — CheckForUpdate on a `go build` binary // reaches it every time — which is why ExplainUpgradeFailure has no case // for it: "rebuild from source" is NoVendorKey's advice, not this one's. DevBuild = coreupd.DevBuild // CandidateNotFound is returned when a requested release candidate does // not exist. CandidateNotFound = coreupd.CandidateNotFound // NotPrerelease is returned when a tag requested as a candidate is // reported as a stable release. NotPrerelease = coreupd.NotPrerelease // DraftRelease is returned for a draft release, which exposes no asset. DraftRelease = coreupd.DraftRelease // InvalidTag is returned for a tag that is not a valid semantic version. InvalidTag = coreupd.InvalidTag // BinaryNotInArchive is returned when a verified archive does not contain // the expected binary. The archive is authentic; it is a packaging fault // rather than an attack. BinaryNotInArchive = coreupd.BinaryNotInArchive // UnknownArchive is returned for an asset whose container format is not // handled. UnknownArchive = coreupd.UnknownArchive // ElevationNotAuthorised is returned when replacing the binary needs // privilege the operator did not opt into. Recoverable — but only through // the opt-in, which ExplainUpgradeFailure names. ElevationNotAuthorised = coreupd.ElevationNotAuthorised // CandidateTagRequired is returned by DownloadCandidate for an empty tag. // It wraps errors.ErrUnsupported, so a caller matching that instead still // matches. CandidateTagRequired = svcupd.CandidateTagRequired // ReleaseMetadataUnreadable is returned when the release API answers // within its cap and the body does not decode. The bytes arrived, so no // transport retry applies. ReleaseMetadataUnreadable = svcupd.ReleaseMetadataUnreadable // ArchiveUnreadable is returned when a release archive will not unpack. It // is raised only AFTER the signature and digest have both verified, so it // is a packaging or transfer fault and never a supply-chain signal. ArchiveUnreadable = svcupd.ArchiveUnreadable // ExecutablePathUnresolved is returned when the process cannot work out // which file on disk it runs from. Nothing is attempted after it. ExecutablePathUnresolved = svcupd.ExecutablePathUnresolved // StagingFailed is returned when the replacement failed BEFORE the rename, // so the running binary is byte-for-byte as it was. That is the whole // difference between this and ReplacementFailed. StagingFailed = svcupd.StagingFailed // ReplacementFailed is returned when the rename over the running // executable did not land. ReplacementFailed = svcupd.ReplacementFailed // ElevationFailed is returned when an escalation the operator DID // authorise was refused by the system. Unlike ElevationNotAuthorised, no // environment variable repairs it. ElevationFailed = svcupd.ElevationFailed // ProbeFailed is returned when the replaced binary did not answer its // probe; rolled_back says whether <binary>.prev was put back. ProbeFailed = svcupd.ProbeFailed )
The sentinels a caller matches with errors.Is, one for each of the codes above.
A facade that publishes a code and withholds the value it identifies is half a surface: a caller can ask errs.HasCode "is this that failure?" and cannot write errors.Is(err, selfupdate.SignatureInvalid), which is the spelling Go programs actually use and the only one that composes with errors.Join. PR #184 fixed exactly this shape in the entitlement facade; lock, cache and authz already publish theirs. This package was the outlier.
They are values rather than type aliases because a sentinel IS a value — there is nothing to alias. The declared type is left off so each keeps its *errs.Error, which is what lock and cache do, and what lets a caller reach Code(), Public() and ExitCode() without a second lookup. Matching works either way: errs.Error.Is compares (Code, Reason), so a sentinel matches a freshly wrapped error of the same identity and not merely the same pointer.
Functions ¶
func StdinIsTerminal ¶
func StdinIsTerminal() bool
StdinIsTerminal reports whether a human could answer a prompt on this process's standard input.
It is a free function rather than something AuthoriseUnattendedUpgrade works out for itself, and that is the point: the consent decision stays testable without a pty, because the CALLER supplies the answer. Pass the result as the interactive argument.
Types ¶
type Candidate ¶
type Candidate = svcupd.CandidateValue
Candidate is one release candidate. It aliases the core value type.
type FileSystem ¶
type FileSystem = coreupd.FileSystem
FileSystem is the disk half of replacing a running binary. It aliases the core port.
type Service ¶
Service replaces the running binary with a newer signed release. It aliases the service type — the engine handle, per ADR 0074.
func New ¶
New returns a Service for the given running version and release source.
The returned Service carries NO vendor key and therefore installs nothing: chain WithVendorKey with the build's linked-in anchor. That is the safe direction — a Service that verified only when a key happened to be present would make the security property depend on a build flag.
func NewWithDeps ¶
NewWithDeps returns a Service with its three ports injected, for a caller that supplies its own HTTP policy or a test that supplies doubles. A nil fs or copier is legal on paths that never reach the disk.
type Source ¶
type Source = svcupd.SourceValue
Source says where releases come from and what they are called. It aliases the service type: these are one engine's construction parameters, which ADR 0074 places with the engine rather than in the contract layer.
type Update ¶
type Update = svcupd.UpdateValue
Update is the outcome of a version check or an install. It aliases the core value type.