Compare commits

...
5 Commits
Author SHA1 Message Date
moloch-- 6a7bd0803a Update --save help 2026-01-28 17:21:35 -08:00
moloch-- 6c70c1c1e6 Add signature validation logic 2026-01-28 17:20:24 -08:00
moloch-- ddc222b8a2 Improvements to update 2026-01-28 17:07:47 -08:00
moloch-- 5506a582ac Add SliverPublicKey 2026-01-28 16:45:32 -08:00
Joe 63d872c98b Merge pull request #2142 from BishopFox/fix/sgn
Fix/sgn
2026-01-28 17:31:46 -07:00
5 changed files with 487 additions and 57 deletions

No files matched your search

+3
View File
@@ -29,11 +29,14 @@ MIN_SUPPORTED_GO_MAJOR_VERSION = 1
MIN_SUPPORTED_GO_MINOR_VERSION = 24
GO_VERSION_VALIDATION_ERR_MSG = Golang version is not supported, please update to at least $(MIN_SUPPORTED_GO_MAJOR_VERSION).$(MIN_SUPPORTED_GO_MINOR_VERSION)
SLIVER_PUBLIC_KEY ?= RWTZPg959v3b7tLG7VzKHRB1/QT+d3c71Uzetfa44qAoX5rH7mGoQTTR
ARMORY_PUBLIC_KEY ?= RWSBpxpRWDrD7Fe+VvRE3c2VEDC2NK80rlNCj+BX0gz44Xw07r6KQD9L
ARMORY_REPO_URL ?= https://api.github.com/repos/sliverarmory/armory/releases
CLIENT_ASSETS_PKG = github.com/bishopfox/sliver/client/assets
SLIVER_UPDATE_PKG = github.com/bishopfox/sliver/client/command/update
LDFLAGS = -ldflags "-s -w \
-X $(SLIVER_UPDATE_PKG).SliverPublicKey=$(SLIVER_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
+2 -1
View File
@@ -25,8 +25,9 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.Bind("update", false, updateCmd, func(f *pflag.FlagSet) {
f.BoolP("prereleases", "P", false, "include pre-released (unstable) versions")
f.StringP("proxy", "p", "", "specify a proxy url (e.g. http://localhost:8080)")
f.StringP("save", "s", "", "save downloaded files to specific directory (default user home dir)")
f.StringP("save", "s", "", "save downloaded files to specific directory (default downloads dir)")
f.BoolP("insecure", "I", false, "skip tls certificate validation")
f.Bool("force", false, "skip version comparison and download the latest release")
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.BindFlagCompletions(updateCmd, func(comp *carapace.ActionMap) {
+408 -36
View File
@@ -21,6 +21,7 @@ package update
import (
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"net"
@@ -40,8 +41,19 @@ import (
"github.com/bishopfox/sliver/client/version"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/util"
"github.com/bishopfox/sliver/util/minisign"
"github.com/cheggaaa/pb/v3"
"github.com/spf13/cobra"
"golang.org/x/term"
)
var (
SliverPublicKey string
)
const (
downloadAttempts = 3
downloadBackoff = 750 * time.Millisecond
)
// UpdateCmd - Check for updates.
@@ -94,7 +106,13 @@ func UpdateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
con.Printf("\nChecking for updates ... ")
prereleases, _ := cmd.Flags().GetBool("prereleases")
release, err := version.CheckForUpdates(client, prereleases)
force, _ := cmd.Flags().GetBool("force")
var release *version.Release
if force {
release, err = version.LatestRelease(client, prereleases)
} else {
release, err = version.CheckForUpdates(client, prereleases)
}
con.Printf("done!\n\n")
if err != nil {
con.PrintErrorf("Update check failed %s\n", err)
@@ -107,9 +125,13 @@ func UpdateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
con.PrintErrorf("%s\n", err)
return
}
updateAvailable(con, client, release, saveTo)
updateAvailable(con, client, release, saveTo, force)
} else {
con.PrintInfof("No new releases.\n")
if force {
con.PrintInfof("No releases found.\n")
} else {
con.PrintInfof("No new releases.\n")
}
}
now := time.Now()
lastCheck := fmt.Appendf(nil, "%d", now.Unix())
@@ -191,84 +213,434 @@ func findAssetFor(prefix string, suffixes []string, assets []version.Asset) *ver
}
func serverAssetForGOOS(assets []version.Asset) *version.Asset {
suffixes := []string{fmt.Sprintf("_%s.zip", runtime.GOOS), runtime.GOOS}
if runtime.GOOS == "darwin" {
suffixes = []string{"_macos.zip", "_macos"}
if runtime.GOARCH == "arm64" {
suffixes = []string{"_macos-arm64.zip", "_macos-arm64"}
}
}
suffixes := assetSuffixes(runtime.GOOS, runtime.GOARCH)
prefix := "sliver-server"
return findAssetFor(prefix, suffixes, assets)
}
func clientAssetForGOOS(assets []version.Asset) *version.Asset {
suffixes := []string{fmt.Sprintf("_%s.zip", runtime.GOOS), runtime.GOOS}
if runtime.GOOS == "darwin" {
suffixes = []string{"_macos.zip", "_macos"}
if runtime.GOARCH == "arm64" {
suffixes = []string{"_macos-arm64.zip", "_macos-arm64"}
}
}
suffixes := assetSuffixes(runtime.GOOS, runtime.GOARCH)
prefix := "sliver-client"
return findAssetFor(prefix, suffixes, assets)
}
func updateAvailable(con *console.SliverClient, client *http.Client, release *version.Release, saveTo string) {
func assetSuffixes(goos, goarch string) []string {
suffixes := make([]string, 0, 8)
seen := make(map[string]struct{}, 8)
add := func(suffix string) {
if suffix == "" {
return
}
if _, ok := seen[suffix]; ok {
return
}
seen[suffix] = struct{}{}
suffixes = append(suffixes, suffix)
}
add(fmt.Sprintf("_%s-%s.zip", goos, goarch))
add(fmt.Sprintf("_%s-%s", goos, goarch))
add(fmt.Sprintf("_%s.zip", goos))
add(fmt.Sprintf("_%s", goos))
if goos == "darwin" {
add(fmt.Sprintf("_macos-%s.zip", goarch))
add(fmt.Sprintf("_macos-%s", goarch))
add("_macos.zip")
add("_macos")
}
return suffixes
}
func updateAvailable(con *console.SliverClient, client *http.Client, release *version.Release, saveTo string, force bool) {
serverAsset := serverAssetForGOOS(release.Assets)
clientAsset := clientAssetForGOOS(release.Assets)
con.Printf("New version available %s\n", release.TagName)
msg := "New version available"
if force {
msg = "Latest release available"
}
con.Printf("%s %s\n", msg, release.TagName)
if serverAsset != nil {
con.Printf(" - Server: %s\n", util.ByteCountBinary(int64(serverAsset.Size)))
}
if clientAsset != nil {
con.Printf(" - Client: %s\n", util.ByteCountBinary(int64(clientAsset.Size)))
}
if serverAsset == nil {
con.PrintWarnf("No server asset available for %s/%s\n", runtime.GOOS, runtime.GOARCH)
}
if clientAsset == nil {
con.PrintWarnf("No client asset available for %s/%s\n", runtime.GOOS, runtime.GOARCH)
}
con.Println()
confirm := false
forms.Confirm("Download update?", &confirm)
if confirm {
con.Printf("Please wait ...")
err := downloadAsset(client, serverAsset, saveTo)
if serverAsset == nil && clientAsset == nil {
con.PrintWarnf("No matching assets found for %s/%s\n", runtime.GOOS, runtime.GOARCH)
return
}
publicKey, err := loadSliverPublicKey()
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
err = downloadAsset(client, clientAsset, saveTo)
if err != nil {
con.PrintErrorf("%s\n", err)
return
downloads := []struct {
label string
asset *version.Asset
}{
{label: "server", asset: serverAsset},
{label: "client", asset: clientAsset},
}
for _, item := range downloads {
if item.asset == nil {
continue
}
con.Printf("Downloading %s update from %s ...\n", item.label, item.asset.BrowserDownloadURL)
if err := downloadAssetWithSignature(con, client, item.asset, release.Assets, saveTo, publicKey); err != nil {
con.PrintErrorf("Failed to download %s update: %s\n", item.label, err)
return
}
con.Println(console.Clearln)
}
con.Println(console.Clearln)
con.PrintInfof("Saved updates to: %s\n", saveTo)
}
}
func downloadAsset(client *http.Client, asset *version.Asset, saveTo string) error {
func downloadAssetWithSignature(con *console.SliverClient, client *http.Client, asset *version.Asset, assets []version.Asset, saveTo string, publicKey minisign.PublicKey) error {
if asset == nil {
return errors.New("no asset available for this platform")
}
downloadURL, err := url.Parse(asset.BrowserDownloadURL)
if err != nil {
return err
}
assetFileName := filepath.Base(downloadURL.Path)
finalPath := filepath.Join(saveTo, assetFileName)
limit := int64(asset.Size)
writer, err := os.Create(filepath.Join(saveTo, assetFileName))
sigURL := asset.BrowserDownloadURL + ".minisig"
var sigLimit int64
if sigAsset := signatureAssetFor(assetFileName, assets); sigAsset != nil && sigAsset.BrowserDownloadURL != "" {
sigURL = sigAsset.BrowserDownloadURL
sigLimit = int64(sigAsset.Size)
}
assetTempPath, err := downloadWithRetries(client, asset.BrowserDownloadURL, saveTo, int64(asset.Size), "asset")
if err != nil {
return fmt.Errorf("download asset: %w", err)
}
sigData, err := downloadBytesWithRetries(client, sigURL, sigLimit, "signature")
if err != nil {
cleanupAssetFiles(assetTempPath, finalPath)
return fmt.Errorf("download signature: %w", err)
}
if err := verifyMinisignSignature(assetTempPath, sigData, assetFileName, publicKey); err != nil {
cleanupAssetFiles(assetTempPath, finalPath)
return err
}
defer writer.Close()
con.PrintSuccessf("Signature verified for %s\n", finalPath)
resp, err := client.Get(asset.BrowserDownloadURL)
if err != nil {
if err := replaceFile(assetTempPath, finalPath); err != nil {
os.Remove(assetTempPath)
return err
}
return nil
}
func signatureAssetFor(assetFileName string, assets []version.Asset) *version.Asset {
want := assetFileName + ".minisig"
for _, asset := range assets {
if asset.Name == want {
return &asset
}
}
return nil
}
func loadSliverPublicKey() (minisign.PublicKey, error) {
if strings.TrimSpace(SliverPublicKey) == "" {
return minisign.PublicKey{}, errors.New("minisign public key not set at build time")
}
var publicKey minisign.PublicKey
if err := publicKey.UnmarshalText([]byte(SliverPublicKey)); err != nil {
return minisign.PublicKey{}, fmt.Errorf("invalid minisign public key: %w", err)
}
return publicKey, nil
}
type httpStatusError struct {
url string
status int
}
func (e httpStatusError) Error() string {
return fmt.Sprintf("unexpected HTTP status %d for %s", e.status, e.url)
}
func downloadWithRetries(client *http.Client, downloadURL, saveTo string, limit int64, label string) (string, error) {
var lastErr error
backoff := downloadBackoff
for attempt := 1; attempt <= downloadAttempts; attempt++ {
path, err := downloadOnce(client, downloadURL, saveTo, limit, label)
if err == nil {
return path, nil
}
lastErr = err
if !isRetryableDownloadError(err) || attempt == downloadAttempts {
break
}
time.Sleep(backoff)
backoff *= 2
}
return "", lastErr
}
func downloadBytesWithRetries(client *http.Client, downloadURL string, limit int64, label string) ([]byte, error) {
var lastErr error
backoff := downloadBackoff
for attempt := 1; attempt <= downloadAttempts; attempt++ {
data, err := downloadBytesOnce(client, downloadURL, limit, label)
if err == nil {
return data, nil
}
lastErr = err
if !isRetryableDownloadError(err) || attempt == downloadAttempts {
break
}
time.Sleep(backoff)
backoff *= 2
}
return nil, lastErr
}
func downloadOnce(client *http.Client, downloadURL, saveTo string, limit int64, label string) (string, error) {
parsedURL, err := url.Parse(downloadURL)
if err != nil {
return "", err
}
baseName := filepath.Base(parsedURL.Path)
tmpFile, err := os.CreateTemp(saveTo, baseName+".tmp-*")
if err != nil {
return "", err
}
success := false
defer func() {
if !success {
tmpFile.Close()
os.Remove(tmpFile.Name())
}
}()
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, downloadURL, nil)
if err != nil {
return "", err
}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
bar := pb.Full.Start64(limit)
barReader := bar.NewProxyReader(resp.Body)
io.Copy(writer, barReader)
bar.Finish()
if resp.StatusCode != http.StatusOK {
return "", httpStatusError{url: downloadURL, status: resp.StatusCode}
}
if limit <= 0 && resp.ContentLength > 0 {
limit = resp.ContentLength
}
var reader io.Reader = resp.Body
var bar *pb.ProgressBar
if limit > 0 {
bar = pb.Full.Start64(limit)
bar.Set("prefix", fmt.Sprintf("%s: ", label))
bar.Set(pb.Bytes, true)
bar.Set(pb.CleanOnFinish, true)
bar.Set(pb.ReturnSymbol, "\r")
if term.IsTerminal(int(os.Stdin.Fd())) {
bar.Set(pb.Terminal, true)
}
reader = bar.NewProxyReader(reader)
}
n, copyErr := io.Copy(tmpFile, reader)
if bar != nil {
bar.Finish()
}
if copyErr != nil {
return "", copyErr
}
if limit > 0 && n != limit {
return "", fmt.Errorf("downloaded %d bytes, expected %d", n, limit)
}
if err := tmpFile.Sync(); err != nil {
return "", err
}
if err := tmpFile.Close(); err != nil {
return "", err
}
success = true
return tmpFile.Name(), nil
}
func downloadBytesOnce(client *http.Client, downloadURL string, limit int64, label string) ([]byte, error) {
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, downloadURL, nil)
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, httpStatusError{url: downloadURL, status: resp.StatusCode}
}
if limit <= 0 && resp.ContentLength > 0 {
limit = resp.ContentLength
}
var reader io.Reader = resp.Body
var bar *pb.ProgressBar
if limit > 0 {
bar = pb.Full.Start64(limit)
bar.Set("prefix", fmt.Sprintf("%s: ", label))
bar.Set(pb.Bytes, true)
bar.Set(pb.CleanOnFinish, true)
bar.Set(pb.ReturnSymbol, "\r")
if term.IsTerminal(int(os.Stdin.Fd())) {
bar.Set(pb.Terminal, true)
}
reader = bar.NewProxyReader(reader)
}
data, readErr := readAllWithLimit(reader, limit)
if bar != nil {
bar.Finish()
}
if readErr != nil {
return nil, readErr
}
return data, nil
}
func isRetryableDownloadError(err error) bool {
if err == nil {
return false
}
if errors.Is(err, io.ErrUnexpectedEOF) {
return true
}
var netErr net.Error
if errors.As(err, &netErr) {
return netErr.Timeout() || netErr.Temporary()
}
var statusErr httpStatusError
if errors.As(err, &statusErr) {
switch statusErr.status {
case http.StatusTooManyRequests, http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout:
return true
default:
return false
}
}
return false
}
func verifyMinisignSignature(artifactPath string, sigData []byte, expectedFile string, publicKey minisign.PublicKey) error {
artifact, err := os.Open(artifactPath)
if err != nil {
return fmt.Errorf("open artifact: %w", err)
}
defer artifact.Close()
reader := minisign.NewReader(artifact)
if _, err := io.Copy(io.Discard, reader); err != nil {
return fmt.Errorf("stream artifact: %w", err)
}
if !reader.Verify(publicKey, sigData) {
return errors.New("signature verification failed")
}
if err := verifyTrustedComment(sigData, expectedFile); err != nil {
return err
}
return nil
}
func readAllWithLimit(reader io.Reader, limit int64) ([]byte, error) {
if limit <= 0 {
return io.ReadAll(reader)
}
lr := &io.LimitedReader{R: reader, N: limit + 1}
data, err := io.ReadAll(lr)
if err != nil {
return nil, err
}
if int64(len(data)) > limit {
return nil, fmt.Errorf("downloaded %d bytes, expected %d", len(data), limit)
}
if int64(len(data)) != limit {
return nil, fmt.Errorf("downloaded %d bytes, expected %d", len(data), limit)
}
return data, nil
}
func verifyTrustedComment(signature []byte, expectedFile string) error {
var sig minisign.Signature
if err := sig.UnmarshalText(signature); err != nil {
return fmt.Errorf("parse signature: %w", err)
}
fileField := trustedCommentFile(sig.TrustedComment)
if fileField == "" {
return fmt.Errorf("trusted comment missing file field: %q", sig.TrustedComment)
}
if fileField != expectedFile {
return fmt.Errorf("trusted comment file mismatch: expected %q, got %q", expectedFile, fileField)
}
return nil
}
func trustedCommentFile(trustedComment string) string {
for _, field := range strings.Fields(trustedComment) {
if after, ok := strings.CutPrefix(field, "file:"); ok {
return after
}
}
return ""
}
func replaceFile(src, dst string) error {
if err := os.Remove(dst); err != nil && !os.IsNotExist(err) {
return err
}
return os.Rename(src, dst)
}
func cleanupAssetFiles(tempPath, finalPath string) {
if tempPath != "" {
_ = os.Remove(tempPath)
}
if finalPath != "" {
_ = os.Remove(finalPath)
}
}
+9
View File
@@ -227,6 +227,15 @@ func (con *SliverClient) PrintSuccessf(format string, args ...any) {
con.printf(Clearln+Success+format, args...)
}
// PrintSuccess prints a success message with a default or provided message.
func (con *SliverClient) PrintSuccess(args ...any) {
if len(args) == 0 {
con.PrintSuccessf("Success")
return
}
con.PrintSuccessf("%s", fmt.Sprint(args...))
}
// PrintWarnf a warning message immediately below the last line of output.
func (con *SliverClient) PrintWarnf(format string, args ...any) {
logger := slog.New(con.jsonHandler)
+65 -20
View File
@@ -63,6 +63,68 @@ func (r *Release) Published() (time.Time, error) {
// occurs we don't really try to recover. If client is nil we just use the Go
// default client with default settings.
func CheckForUpdates(client *http.Client, prereleases bool) (*Release, error) {
releases, err := fetchReleases(client)
if err != nil {
return nil, err
}
if len(releases) == 0 {
return nil, nil
}
current := canonicalSemver(Version)
if current == "" {
current = "v0.0.0"
}
for _, release := range releases {
if release == nil {
continue
}
if release.Prerelease && !prereleases {
continue
}
releaseVersion := canonicalSemver(release.TagName)
if releaseVersion == "" {
continue
}
if semver.Compare(releaseVersion, current) > 0 {
return release, nil
}
}
return nil, nil
}
// LatestRelease - Returns the latest release without comparing to the current version.
func LatestRelease(client *http.Client, prereleases bool) (*Release, error) {
releases, err := fetchReleases(client)
if err != nil {
return nil, err
}
if len(releases) == 0 {
return nil, nil
}
var latest *Release
var latestVersion string
for _, release := range releases {
if release == nil {
continue
}
if release.Prerelease && !prereleases {
continue
}
releaseVersion := canonicalSemver(release.TagName)
if releaseVersion == "" {
continue
}
if latest == nil || semver.Compare(releaseVersion, latestVersion) > 0 {
latest = release
latestVersion = releaseVersion
}
}
return latest, nil
}
func fetchReleases(client *http.Client) ([]*Release, error) {
skip := os.Getenv(skipCheckEnv)
if skip != "" || GithubReleasesURL == "" {
return nil, nil
@@ -84,27 +146,10 @@ func CheckForUpdates(client *http.Client, prereleases bool) (*Release, error) {
if resp.StatusCode != 200 {
return nil, errors.New("API returned non-200 status code")
}
releases := &[]*Release{}
err = json.Unmarshal(body, releases)
if err != nil {
releases := []*Release{}
if err := json.Unmarshal(body, &releases); err != nil {
return nil, err
}
current := canonicalSemver(Version)
if current == "" {
current = "v0.0.0"
}
for _, release := range *releases {
if release.Prerelease && !prereleases {
continue
}
releaseVersion := canonicalSemver(release.TagName)
if releaseVersion == "" {
continue
}
if semver.Compare(releaseVersion, current) > 0 {
return release, nil
}
}
return nil, nil
return releases, nil
}