selfupdate

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Mar 27, 2023 License: Apache-2.0 Imports: 23 Imported by: 10

README

self-update: Build self-updating Fyne programs

godoc reference Coverage Status

Package update provides functionality to implement secure, self-updating Fyne programs (or other single-file targets)

A program can update itself by replacing its executable file with a new version.

It provides the flexibility to implement different updating user experiences like auto-updating, or manual user-initiated updates. It also boasts advanced features like binary patching and code signing verification.

Unmanaged update

Example of updating from a URL:

import (
    "fmt"
    "net/http"

    "github.com/fynelabs/selfupdate"
)

func doUpdate(url string) error {
    resp, err := http.Get(url)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    err = update.Apply(resp.Body, update.Options{})
    if err != nil {
        // error handling
    }
    return err
}

Managed update

To help make self updating Fyne application a new API and a tool, selfupdatectl have been introduced. The new API allow to provide a source where to get an update, configure a schedule for the update and a ed25519 public key to ensure that the update is only applied if they come from the proper source.

Example with the new API:

import (
	"crypto/ed25519"
	"log"
	"time"

	"github.com/fynelabs/selfupdate"
)

func main() {
	done := make(chan struct{}, 2)

	// Used `selfupdatectl create-keys` followed by `selfupdatectl print-key`
	publicKey := ed25519.PublicKey{178, 103, 83, 57, 61, 138, 18, 249, 244, 80, 163, 162, 24, 251, 190, 241, 11, 168, 179, 41, 245, 27, 166, 70, 220, 254, 118, 169, 101, 26, 199, 129}

	// The public key above match the signature of the below file served by our CDN
	httpSource := selfupdate.NewHTTPSource(nil, "http://localhost/{{.Executable}}-{{.OS}}-{{.Arch}}{{.Ext}}")
	config := &selfupdate.Config{
		Source:    httpSource,
		Schedule:  selfupdate.Schedule{FetchOnStart: true, Interval: time.Minute * time.Duration(60)},
		PublicKey: publicKey,

		ProgressCallback: func(f float64, err error) { log.Println("Download", f, "%") },
		RestartConfirmCallback: func() bool { return true},
		UpgradeConfirmCallback: func(_ string) bool { return true },
		ExitCallback: func(_ error) { os.Exit(1) }
	}

	_, err := selfupdate.Manage(config)
	if err != nil {
		log.Println("Error while setting up update manager: ", err)
		return
	}

	<-done
}

If you desire a GUI element and visual integration with Fyne, you should check fyneselfupdate.

To help you manage your key, sign binary and upload them to an online S3 bucket the selfupdatectl tool is provided. You can check its documentation here.

Logging

We provide three package wide variables: LogError, LogInfo and LogDebug that follow log.Printf API to provide an easy way to hook any logger in. To use it with go logger, you can just do

selfupdate.LogError = log.Printf

If you are using logrus for example, you could do the following:

selfupdate.LogError = logrus.Errorf
selfupdate.LogInfo = logrus.Infof
selfupdate.LogDebug = logrus.Debugf

Most logger module in the go ecosystem do provide an API that match the log.Printf and it should be straight forward to use in the same way as with logrus.

Features

  • Cross platform support
  • Binary patch application
  • Checksum verification
  • Code signing verification
  • Support for updating arbitrary files

API Compatibility Promises

The main branch of selfupdate is not guaranteed to have a stable API over time. Still we will try hard to not break its API unecessarily and will follow a proper versioning of our release when necessary.

The selfupdate package makes the following promises about API compatibility:

  1. A list of all API-breaking changes will be documented in this README.
  2. selfupdate will strive for as few API-breaking changes as possible.

API Breaking Changes

  • May 30, 2022: Many changes moving to a new API that will be supported going forward.
  • June 22, 2022: First tagged release, v0.1.0.

License

Apache

Sponsors

This project is kindly sponsored by the following companies:

Documentation

Overview

Package selfupdate provides functionality to implement secure, self-updating Go programs (or other single-file targets).

For complete updating solutions please see Equinox (https://equinox.io) and go-tuf (https://github.com/flynn/go-tuf).

Basic Example

This example shows how to update a program remotely from a URL.

import (
	"fmt"
	"net/http"

	"github.com/fynelabs/selfupdate"
)

func doUpdate(url string) error {
	// request the new file
	resp, err := http.Get(url)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	err := update.Apply(resp.Body, update.Options{})
	if err != nil {
		if rerr := update.RollbackError(err); rerr != nil {
			fmt.Println("Failed to rollback from bad update: %v", rerr)
		}
	}
	return err
}

Binary Patching

Go binaries can often be large. It can be advantageous to only ship a binary patch to a client instead of the complete program text of a new version.

This example shows how to update a program with a bsdiff binary patch. Other patch formats may be applied by implementing the Patcher interface.

import (
	"encoding/hex"
	"io"

	"github.com/fynelabs/selfupdate"
)

func updateWithPatch(patch io.Reader) error {
	err := update.Apply(patch, update.Options{
		Patcher: update.NewBSDiffPatcher()
	})
	if err != nil {
		// error handling
	}
	return err
}

Checksum Verification

Updating executable code on a computer can be a dangerous operation unless you take the appropriate steps to guarantee the authenticity of the new code. While checksum verification is important, it should always be combined with signature verification (next section) to guarantee that the code came from a trusted party.

selfupdate validates SHA256 checksums by default, but this is pluggable via the Hash property on the Options struct.

This example shows how to guarantee that the newly-updated binary is verified to have an appropriate checksum (that was otherwise retrived via a secure channel) specified as a hex string.

import (
	"crypto"
	_ "crypto/sha256"
	"encoding/hex"
	"io"

	"github.com/fynelabs/selfupdate"
)

func updateWithChecksum(binary io.Reader, hexChecksum string) error {
	checksum, err := hex.DecodeString(hexChecksum)
	if err != nil {
		return err
	}
	err = update.Apply(binary, update.Options{
		Hash: crypto.SHA256, 	// this is the default, you don't need to specify it
		Checksum: checksum,
	})
	if err != nil {
		// error handling
	}
	return err
}

Cryptographic Signature Verification

Cryptographic verification of new code from an update is an extremely important way to guarantee the security and integrity of your updates.

Verification is performed by validating the signature of a hash of the new file. This means nothing changes if you apply your update with a patch.

This example shows how to add signature verification to your updates. To make all of this work an application distributor must first create a public/private key pair and embed the public key into their application. When they issue a new release, the issuer must sign the new executable file with the private key and distribute the signature along with the update.

import (
	"crypto"
	_ "crypto/sha256"
	"encoding/hex"
	"io"

	"github.com/fynelabs/selfupdate"
)

var publicKey = []byte(`
-----BEGIN PUBLIC KEY-----
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEtrVmBxQvheRArXjg2vG1xIprWGuCyESx
MMY8pjmjepSy2kuz+nl9aFLqmr+rDNdYvEBqQaZrYMc6k29gjvoQnQ==
-----END PUBLIC KEY-----
`)

func verifiedUpdate(binary io.Reader, hexChecksum, hexSignature string) {
	checksum, err := hex.DecodeString(hexChecksum)
	if err != nil {
		return err
	}
	signature, err := hex.DecodeString(hexSignature)
	if err != nil {
		return err
	}
	opts := update.Options{
		Checksum: checksum,
		Signature: signature,
		Hash: crypto.SHA256, 	                 // this is the default, you don't need to specify it
		Verifier: update.NewECDSAVerifier(),   // this is the default, you don't need to specify it
	}
	err = opts.SetPublicKeyPEM(publicKey)
	if err != nil {
		return err
	}
	err = update.Apply(binary, opts)
	if err != nil {
		// error handling
	}
	return err
}

Building Single-File Go Binaries

In order to update a Go application with self-update, you must distributed it as a single executable. This is often easy, but some applications require static assets (like HTML and CSS asset files or TLS certificates). In order to update applications like these, you'll want to make sure to embed those asset files into the distributed binary with a tool like go-bindata (my favorite): https://github.com/jteeuwen/go-bindata

Non-Goals

Mechanisms and protocols for determining whether an update should be applied and, if so, which one are out of scope for this package. Please consult go-tuf (https://github.com/flynn/go-tuf) or Equinox (https://equinox.io) for more complete solutions.

selfupdate only works for self-updating applications that are distributed as a single binary, i.e. applications that do not have additional assets or dependency files. Updating application that are distributed as mutliple on-disk files is out of scope, although this may change in future versions of this library.

Index

Constants

This section is empty.

Variables

View Source
var ErrNotSupported = errors.New("operating system not supported")

ErrNotSupported is returned by `Manage` when it is not possible to manage the current application.

View Source
var LogDebug func(string, ...interface{})

LogDebug will be called to log any reason that prevented an executable update, because there wasn't any available detected

View Source
var LogError func(string, ...interface{})

LogError will be called to log any reason that have prevented an executable update

View Source
var LogInfo func(string, ...interface{})

LogInfo will be called to log any reason that prevented an executable update due to a "user" decision via one of the callback

Functions

func Apply

func Apply(update io.Reader, opts Options) error

Apply performs an update of the current executable (or opts.TargetFile, if set) with the contents of the given io.Reader.

Apply performs the following actions to ensure a safe cross-platform update:

1. If configured, applies the contents of the update io.Reader as a binary patch.

2. If configured, computes the checksum of the new executable and verifies it matches.

3. If configured, verifies the signature with a public key.

4. Creates a new file, /path/to/.target.new with the TargetMode with the contents of the updated file

5. Renames /path/to/target to /path/to/.target.old

6. Renames /path/to/.target.new to /path/to/target

7. If the final rename is successful, deletes /path/to/.target.old, returns no error. On Windows, the removal of /path/to/target.old always fails, so instead Apply hides the old file instead.

8. If the final rename fails, attempts to roll back by renaming /path/to/.target.old back to /path/to/target.

If the roll back operation fails, the file system is left in an inconsistent state (betweet steps 5 and 6) where there is no new executable file and the old executable file could not be be moved to its original location. In this case you should notify the user of the bad news and ask them to recover manually. Applications can determine whether the rollback failed by calling RollbackError, see the documentation on that function for additional detail.

This function is provided for backward compatibility with go-selfupdate original package

func ManualUpdate

func ManualUpdate(s Source, publicKey ed25519.PublicKey) error

ManualUpdate applies a specific update manually instead of managing the update of this app automatically.

func RollbackError

func RollbackError(err error) error

RollbackError takes an error value returned by Apply and returns the error, if any, that occurred when attempting to roll back from a failed update. Applications should always call this function on any non-nil errors returned by Apply.

If no rollback was needed or if the rollback was successful, RollbackError returns nil, otherwise it returns the error encountered when trying to roll back.

Types

type Config

type Config struct {
	Current   *Version          // If present will define the current version of the executable that need update
	Source    Source            // Necessary Source for update
	Schedule  Schedule          // Define when to trigger an update
	PublicKey ed25519.PublicKey // The public key that match the private key used to generate the signature of future update

	ProgressCallback       func(float64, error) // if present will call back with 0.0 at the start, rising through to 1.0 at the end if the progress is known. A negative start number will be sent if size is unknown, any error will pass as is and the process is considered done
	RestartConfirmCallback func() bool          // if present will ask for user acceptance before restarting app
	UpgradeConfirmCallback func(string) bool    // if present will ask for user acceptance, it can present the message passed
	ExitCallback           func(error)          // if present will be expected to handle app exit procedure
}

Config define extra parameter necessary to manage the updating process

type HTTPSource

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

HTTPSource provide a Source that will download the update from a HTTP url. It is expecting the signature file to be served at ${URL}.ed25519

func (*HTTPSource) Get

func (h *HTTPSource) Get(v *Version) (io.ReadCloser, int64, error)

Get will return if it succeed an io.ReaderCloser to the new executable being downloaded and its length

func (*HTTPSource) GetSignature

func (h *HTTPSource) GetSignature() ([64]byte, error)

GetSignature will return the content of ${URL}.ed25519

func (*HTTPSource) LatestVersion

func (h *HTTPSource) LatestVersion() (*Version, error)

LatestVersion will return the URL Last-Modified time

type Options

type Options struct {
	// TargetPath defines the path to the file to update.
	// The emptry string means 'the executable file of the running program'.
	TargetPath string

	// Create TargetPath replacement with this file mode. If zero, defaults to 0755.
	TargetMode os.FileMode

	// Checksum of the new binary to verify against. If nil, no checksum or signature verification is done.
	Checksum []byte

	// Public key to use for signature verification. If nil, no signature verification is done.
	PublicKey crypto.PublicKey

	// Signature to verify the updated file. If nil, no signature verification is done.
	Signature []byte

	// Pluggable signature verification algorithm. If nil, ECDSA is used.
	Verifier Verifier

	// Use this hash function to generate the checksum. If not set, SHA256 is used.
	Hash crypto.Hash

	// If nil, treat the update as a complete replacement for the contents of the file at TargetPath.
	// If non-nil, treat the update contents as a patch and use this object to apply the patch.
	Patcher Patcher

	// Store the old executable file at this path after a successful update.
	// The empty string means the old executable file will be removed after the update.
	OldSavePath string
}

Options give additional parameters when calling Apply

func (*Options) CheckPermissions

func (o *Options) CheckPermissions() error

CheckPermissions determines whether the process has the correct permissions to perform the requested update. If the update can proceed, it returns nil, otherwise it returns the error that would occur if an update were attempted.

func (*Options) SetPublicKeyPEM

func (o *Options) SetPublicKeyPEM(pembytes []byte) error

SetPublicKeyPEM is a convenience method to set the PublicKey property used for checking a completed update's signature by parsing a Public Key formatted as PEM data.

type Patcher

type Patcher interface {
	Patch(old io.Reader, new io.Writer, patch io.Reader) error
}

Patcher defines an interface for applying binary patches to an old item to get an updated item.

func NewBSDiffPatcher

func NewBSDiffPatcher() Patcher

NewBSDiffPatcher returns a new Patcher that applies binary patches using the bsdiff algorithm. See http://www.daemonology.net/bsdiff/

type Repeating

type Repeating int

Repeating pattern for scheduling update at a specific time

const (
	// None will not schedule
	None Repeating = iota
	// Hourly will schedule in the next hour and repeat it every hour after
	Hourly
	// Daily will schedule next day and repeat it every day after
	Daily
	// Monthly will schedule next month and repeat it every month after
	Monthly
)

type Schedule

type Schedule struct {
	FetchOnStart bool          // Trigger when the updater is created
	Interval     time.Duration // Trigger at regular interval
	At           ScheduleAt    // Trigger at a specific time
}

Schedule define when to trigger an update

type ScheduleAt

type ScheduleAt struct {
	Repeating // The pattern to enforce for the repeating schedule
	time.Time // Offset time used to define when in a minute/hour/day/month to actually trigger the schedule
}

ScheduleAt define when a repeating update at a specific time should be triggered

type Source

type Source interface {
	Get(*Version) (io.ReadCloser, int64, error) // Get the executable to be updated to
	GetSignature() ([64]byte, error)            // Get the signature that match the executable
	LatestVersion() (*Version, error)           // Get the latest version information to determine if we should trigger an update
}

Source define an interface that is able to get an update

func NewHTTPSource

func NewHTTPSource(client *http.Client, base string) Source

NewHTTPSource provide a selfupdate.Source that will fetch the specified base URL for update and signature using the http.Client provided. To help into providing cross platform application, the base is actually a Go Template string where the following parameter are recognized: {{.OS}} will be filled by the runtime OS name {{.Arch}} will be filled by the runtime Arch name {{.Ext}} will be filled by the executable expected extension for the OS As an example the following string `http://localhost/myapp-{{.OS}}-{{.Arch}}{{.Ext}}` would fetch on Windows AMD64 the following URL: `http://localhost/myapp-windows-amd64.exe` and on Linux AMD64: `http://localhost/myapp-linux-amd64`.

type Updater

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

Updater is managing update for your application in the background

func Manage

func Manage(conf *Config) (*Updater, error)

Manage sets up an Updater and runs it to manage the current executable.

func (*Updater) CheckNow

func (u *Updater) CheckNow() error

CheckNow will manually trigger a check of an update and if one is present will start the update process

func (*Updater) Restart

func (u *Updater) Restart() error

Restart once an update is done can trigger a restart of the binary. This is useful to implement a restart later policy.

type Verifier

type Verifier interface {
	VerifySignature(checksum, signature []byte, h crypto.Hash, publicKey crypto.PublicKey) error
}

Verifier defines an interface for verfiying an update's signature with a public key.

func NewECDSAVerifier

func NewECDSAVerifier() Verifier

NewECDSAVerifier returns a Verifier that uses the ECDSA algorithm to verify updates.

func NewRSAVerifier

func NewRSAVerifier() Verifier

NewRSAVerifier returns a Verifier that uses the RSA algorithm to verify updates.

type Version

type Version struct {
	Number string    // if the app knows its version and supports checking metadata
	Build  int       // if the app has a build number this could be compared
	Date   time.Time // last update, could be mtime
}

Version define an executable versionning information

Directories

Path Synopsis
cmd
internal
binarydist
Package binarydist implements binary diff and patch as described on http://www.daemonology.net/bsdiff/.
Package binarydist implements binary diff and patch as described on http://www.daemonology.net/bsdiff/.
osext
Package osext provide extensions to the standard "os" package.
Package osext provide extensions to the standard "os" package.

Jump to

Keyboard shortcuts

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