Working update code for macos

This commit is contained in:
David Dworken
2022-05-26 23:45:08 -07:00
parent 91a207c6f5
commit 1da703e9c2
5 changed files with 120 additions and 95 deletions

View File

@ -2,7 +2,6 @@ package lib
import (
"bytes"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
@ -23,8 +22,6 @@ import (
_ "embed" // for embedding config.sh
"golang.org/x/sys/unix"
"github.com/glebarez/sqlite" // an alternate non-cgo-requiring sqlite driver
"gorm.io/gorm"
"gorm.io/gorm/logger"
@ -532,7 +529,11 @@ func Update() error {
}
// Verify the SLSA attestation
err = verifyBinary("/tmp/hishtory-client", "/tmp/hishtory-client.intoto.jsonl", downloadData.Version+"-"+runtime.GOOS+"-"+runtime.GOARCH)
if runtime.GOOS == "darwin" {
err = verifyBinaryMac("/tmp/hishtory-client", downloadData)
} else {
err = verifyBinary("/tmp/hishtory-client", "/tmp/hishtory-client.intoto.jsonl", downloadData.Version+"-"+runtime.GOOS+"-"+runtime.GOARCH)
}
if err != nil {
return fmt.Errorf("failed to verify SLSA provenance of the updated binary, aborting update: %v", err)
}
@ -549,14 +550,6 @@ func Update() error {
}
}
// On MacOS, set the xattrs containing the signatures. These are generated by an action and pushed to a github release that we download and set
if runtime.GOOS == "darwin" {
err := setCodesigningXattrs(downloadData, "/tmp/hishtory-client")
if err != nil {
return fmt.Errorf("failed to set codesigning xattrs: %v", err)
}
}
// Install the new one
cmd := exec.Command("chmod", "+x", "/tmp/hishtory-client")
var stdout bytes.Buffer
@ -578,6 +571,98 @@ func Update() error {
return nil
}
func verifyBinaryMac(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 {
panic("TODO: better error message")
}
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(unsignedBinaryPath, "/tmp/hishtory-client.intoto.jsonl", downloadData.Version+"-"+runtime.GOOS+"-"+runtime.GOARCH)
}
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))
}
}
logger := GetLogger()
for _, d := range differences {
logger.Printf("comparing binaries: %#v\n", d)
}
if len(differences) > 2 {
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 (stdout=%#v, stderr%#v): %v", stdout.String(), stderr.String(), err)
}
return nil
}
func downloadFiles(updateInfo shared.UpdateInfo) error {
clientUrl := ""
clientProvenanceUrl := ""
@ -734,79 +819,6 @@ type darwinCodeSignature struct {
Cs string `json:"cs"`
}
func parseXattr(xattrDump string) (darwinCodeSignature, error) {
var xattr darwinCodeSignature
err := json.Unmarshal([]byte(xattrDump), &xattr)
if err != nil {
return xattr, fmt.Errorf("failed to parse xattr: %v", err)
}
if xattr.Cd == "" || xattr.Cr == "" || xattr.Cr1 == "" || xattr.Cs == "" {
return xattr, fmt.Errorf("xattr=%#v has empty attributes, failed to set code signatures", xattr)
}
return xattr, nil
}
func parseHex(input string) []byte {
input = strings.ReplaceAll(input, " ", "")
input = strings.ReplaceAll(input, "\n", "")
data, err := hex.DecodeString(input)
if err != nil {
panic("TODO: wire this up")
}
return data
}
func setXattr(filename, xattrDump string) error {
x, err := parseXattr(xattrDump)
if err != nil {
return fmt.Errorf("failed to parse xattr file: %v", err)
}
err = unix.Setxattr(filename, "com.apple.cs.CodeDirectory", parseHex(x.Cd), 0)
if err != nil {
return fmt.Errorf("failed to set xattr com.apple.cs.CodeDirectory on file %#v: %v", filename, err)
}
err = unix.Setxattr(filename, "com.apple.cs.CodeRequirements", parseHex(x.Cr), 0)
if err != nil {
return fmt.Errorf("failed to set xattr com.apple.cs.CodeRequirements on file %#v: %v", filename, err)
}
err = unix.Setxattr(filename, "com.apple.cs.CodeRequirements-1", parseHex(x.Cr1), 0)
if err != nil {
return fmt.Errorf("failed to set xattr com.apple.cs.CodeRequirements-1 on file %#v: %v", filename, err)
}
err = unix.Setxattr(filename, "com.apple.cs.CodeSignature", parseHex(x.Cs), 0)
if err != nil {
return fmt.Errorf("failed to set xattr com.apple.cs.CodeSignature on file %#v: %v", filename, err)
}
return nil
}
func setCodesigningXattrs(downloadInfo shared.UpdateInfo, filename string) error {
if runtime.GOOS != "darwin" {
return fmt.Errorf("setCodesigningXattrs is only supported on macOS")
}
url := ""
if runtime.GOARCH == "arm64" {
url = downloadInfo.DarwinArm64Xattr
} else if runtime.GOARCH == "amd64" {
url = downloadInfo.DarwinAmd64Xattr
} else {
return fmt.Errorf("setCodesigningXattrs only supports arm64 and amd64: %#v", runtime.GOARCH)
}
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("failed to GET %s: %v", url, err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("failed to GET %s: status_code=%d", url, resp.StatusCode)
}
xattrDump, err := ioutil.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body from GET %s: %v", url, err)
}
return setXattr(filename, string(xattrDump))
}
func IsOfflineError(err error) bool {
return strings.Contains(err.Error(), "dial tcp: lookup api.hishtory.dev") || strings.Contains(err.Error(), "read: connection reset by peer")
}