mirror of
https://github.com/ddworken/hishtory.git
synced 2025-08-18 10:49:54 +02:00
Move a bunch of update-specific code out of the generic lib.go file and into the update command
This commit is contained in:
@@ -12,14 +12,11 @@ import (
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
_ "embed" // for embedding config.sh
|
||||
@@ -383,6 +380,13 @@ func readFileToArray(path string) ([]string, error) {
|
||||
return lines, nil
|
||||
}
|
||||
|
||||
func getServerHostname() string {
|
||||
if server := os.Getenv("HISHTORY_SERVER"); server != "" {
|
||||
return server
|
||||
}
|
||||
return "https://api.hishtory.dev"
|
||||
}
|
||||
|
||||
func GetDownloadData() (shared.UpdateInfo, error) {
|
||||
respBody, err := ApiGet("/api/v1/download")
|
||||
if err != nil {
|
||||
@@ -396,235 +400,6 @@ func GetDownloadData() (shared.UpdateInfo, error) {
|
||||
return downloadData, nil
|
||||
}
|
||||
|
||||
func getTmpClientPath() string {
|
||||
tmpDir := "/tmp/"
|
||||
if os.Getenv("TMPDIR") != "" {
|
||||
tmpDir = os.Getenv("TMPDIR")
|
||||
}
|
||||
return path.Join(tmpDir, "hishtory-client")
|
||||
}
|
||||
|
||||
func Update(ctx context.Context) error {
|
||||
// Download the binary
|
||||
downloadData, err := GetDownloadData()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if downloadData.Version == "v0."+Version {
|
||||
fmt.Printf("Latest version (v0.%s) is already installed\n", Version)
|
||||
return nil
|
||||
}
|
||||
err = downloadFiles(downloadData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify the SLSA attestation
|
||||
var slsaError error
|
||||
if runtime.GOOS == "darwin" {
|
||||
slsaError = verifyBinaryMac(ctx, getTmpClientPath(), downloadData)
|
||||
} else {
|
||||
slsaError = verifyBinary(ctx, getTmpClientPath(), getTmpClientPath()+".intoto.jsonl", downloadData.Version)
|
||||
}
|
||||
if slsaError != nil {
|
||||
err = handleSlsaFailure(slsaError)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Unlink the existing binary so we can overwrite it even though it is still running
|
||||
if runtime.GOOS == "linux" {
|
||||
homedir := hctx.GetHome(ctx)
|
||||
err = syscall.Unlink(path.Join(homedir, data.GetHishtoryPath(), "hishtory"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unlink %s for update: %w", path.Join(homedir, data.GetHishtoryPath(), "hishtory"), err)
|
||||
}
|
||||
}
|
||||
|
||||
// Install the new one
|
||||
cmd := exec.Command("chmod", "+x", getTmpClientPath())
|
||||
var stdout bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to chmod +x the update (stdout=%#v, stderr=%#v): %w", stdout.String(), stderr.String(), err)
|
||||
}
|
||||
cmd = exec.Command(getTmpClientPath(), "install")
|
||||
cmd.Stdout = os.Stdout
|
||||
stderr = bytes.Buffer{}
|
||||
cmd.Stdin = os.Stdin
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to install update (stderr=%#v), is %s in a noexec directory? (if so, set the TMPDIR environment variable): %w", stderr.String(), getTmpClientPath(), err)
|
||||
}
|
||||
fmt.Printf("Successfully updated hishtory from v0.%s to %s\n", Version, downloadData.Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
func verifyBinaryMac(ctx context.Context, binaryPath string, downloadData shared.UpdateInfo) error {
|
||||
// On Mac, binary verification is a bit more complicated since mac binaries are code
|
||||
// signed. To verify a signed binary, we:
|
||||
// 1. Download the unsigned binary
|
||||
// 2. Strip the real signature from the signed binary and the ad-hoc signature from the unsigned binary
|
||||
// 3. Assert that those binaries match
|
||||
// 4. Use SLSA to verify the unsigned binary (pre-strip)
|
||||
// Yes, this is complicated. But AFAICT, it is the only solution here.
|
||||
|
||||
// Step 1: Download the "unsigned" binary that actually has an ad-hoc signature from the
|
||||
// go compiler.
|
||||
unsignedBinaryPath := binaryPath + "-unsigned"
|
||||
var err error = nil
|
||||
if runtime.GOOS == "darwin" && runtime.GOARCH == "amd64" {
|
||||
err = downloadFile(unsignedBinaryPath, downloadData.DarwinAmd64UnsignedUrl)
|
||||
} else if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
|
||||
err = downloadFile(unsignedBinaryPath, downloadData.DarwinArm64UnsignedUrl)
|
||||
} else {
|
||||
err = fmt.Errorf("verifyBinaryMac() called for the unhandled branch GOOS=%s, GOARCH=%s", runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Step 2: Create the .nosig files that have no signatures whatsoever
|
||||
noSigSuffix := ".nosig"
|
||||
err = stripCodeSignature(binaryPath, binaryPath+noSigSuffix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = stripCodeSignature(unsignedBinaryPath, unsignedBinaryPath+noSigSuffix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Step 3: Compare the binaries
|
||||
err = assertIdenticalBinaries(binaryPath+noSigSuffix, unsignedBinaryPath+noSigSuffix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Step 4: Use SLSA to verify the unsigned binary
|
||||
return verifyBinary(ctx, unsignedBinaryPath, getTmpClientPath()+".intoto.jsonl", downloadData.Version)
|
||||
}
|
||||
|
||||
func assertIdenticalBinaries(bin1Path, bin2Path string) error {
|
||||
bin1, err := os.ReadFile(bin1Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bin2, err := os.ReadFile(bin2Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(bin1) != len(bin2) {
|
||||
return fmt.Errorf("unsigned binaries have different lengths (len(%s)=%d, len(%s)=%d)", bin1Path, len(bin1), bin2Path, len(bin2))
|
||||
}
|
||||
differences := make([]string, 0)
|
||||
for i := range bin1 {
|
||||
b1 := bin1[i]
|
||||
b2 := bin2[i]
|
||||
if b1 != b2 {
|
||||
differences = append(differences, fmt.Sprintf("diff at index %d: %s[%d]=%x, %s[%d]=%x", i, bin1Path, i, b1, bin2Path, i, b2))
|
||||
}
|
||||
}
|
||||
for _, d := range differences {
|
||||
hctx.GetLogger().Infof("comparing binaries: %#v\n", d)
|
||||
}
|
||||
if len(differences) > 5 {
|
||||
return fmt.Errorf("found %d differences in the binary", len(differences))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stripCodeSignature(inPath, outPath string) error {
|
||||
_, err := exec.LookPath("codesign_allocate")
|
||||
if err != nil {
|
||||
return fmt.Errorf("your system is missing the codesign_allocate tool, so we can't verify the SLSA attestation (you can bypass this by setting `export HISHTORY_DISABLE_SLSA_ATTESTATION=true` in your shell)")
|
||||
}
|
||||
cmd := exec.Command("codesign_allocate", "-i", inPath, "-o", outPath, "-r")
|
||||
var stdout bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to use codesign_allocate to strip signatures on binary=%v (stdout=%#v, stderr%#v): %w", inPath, stdout.String(), stderr.String(), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func downloadFiles(updateInfo shared.UpdateInfo) error {
|
||||
clientUrl := ""
|
||||
clientProvenanceUrl := ""
|
||||
if runtime.GOOS == "linux" && runtime.GOARCH == "amd64" {
|
||||
clientUrl = updateInfo.LinuxAmd64Url
|
||||
clientProvenanceUrl = updateInfo.LinuxAmd64AttestationUrl
|
||||
} else if runtime.GOOS == "linux" && runtime.GOARCH == "arm64" {
|
||||
clientUrl = updateInfo.LinuxArm64Url
|
||||
clientProvenanceUrl = updateInfo.LinuxArm64AttestationUrl
|
||||
} else if runtime.GOOS == "linux" && runtime.GOARCH == "arm" {
|
||||
clientUrl = updateInfo.LinuxArm7Url
|
||||
clientProvenanceUrl = updateInfo.LinuxArm7AttestationUrl
|
||||
} else if runtime.GOOS == "darwin" && runtime.GOARCH == "amd64" {
|
||||
clientUrl = updateInfo.DarwinAmd64Url
|
||||
clientProvenanceUrl = updateInfo.DarwinAmd64AttestationUrl
|
||||
} else if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
|
||||
clientUrl = updateInfo.DarwinArm64Url
|
||||
clientProvenanceUrl = updateInfo.DarwinArm64AttestationUrl
|
||||
} else {
|
||||
return fmt.Errorf("no update info found for GOOS=%s, GOARCH=%s", runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
err := downloadFile(getTmpClientPath(), clientUrl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = downloadFile(getTmpClientPath()+".intoto.jsonl", clientProvenanceUrl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func downloadFile(filename, url string) error {
|
||||
// Download the data
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download file at %s to %s: %w", url, filename, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("failed to download file at %s due to resp_code=%d", url, resp.StatusCode)
|
||||
}
|
||||
|
||||
// Delete the file if it already exists. This is necessary due to https://openradar.appspot.com/FB8735191
|
||||
if _, err := os.Stat(filename); err == nil {
|
||||
err = os.Remove(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete file %v when trying to download a new version", filename)
|
||||
}
|
||||
}
|
||||
|
||||
// Create the file
|
||||
out, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save file to %s: %w", filename, err)
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func getServerHostname() string {
|
||||
if server := os.Getenv("HISHTORY_SERVER"); server != "" {
|
||||
return server
|
||||
}
|
||||
return "https://api.hishtory.dev"
|
||||
}
|
||||
|
||||
func httpClient() *http.Client {
|
||||
return &http.Client{}
|
||||
}
|
||||
|
Reference in New Issue
Block a user