mirror of
https://github.com/ddworken/hishtory.git
synced 2025-08-19 03:06:45 +02:00
Working update code for macos
This commit is contained in:
1
Makefile
1
Makefile
@@ -36,7 +36,6 @@ release:
|
|||||||
rm .slsa-goreleaser.yml
|
rm .slsa-goreleaser.yml
|
||||||
git add .slsa-goreleaser.yml
|
git add .slsa-goreleaser.yml
|
||||||
git commit -m "Release: finish releasing v0.`cat VERSION`" --no-verify
|
git commit -m "Release: finish releasing v0.`cat VERSION`" --no-verify
|
||||||
git tag v0.`cat VERSION`-xattr
|
|
||||||
git push && git push --tags
|
git push && git push --tags
|
||||||
# Tag the release
|
# Tag the release
|
||||||
gh release create v0.`cat VERSION` --generate-notes
|
gh release create v0.`cat VERSION` --generate-notes
|
||||||
|
@@ -324,7 +324,9 @@ func decrementVersionIfInvalid(initialVersion string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func assertValidUpdate(updateInfo shared.UpdateInfo) error {
|
func assertValidUpdate(updateInfo shared.UpdateInfo) error {
|
||||||
urls := []string{updateInfo.LinuxAmd64Url, updateInfo.LinuxAmd64AttestationUrl, updateInfo.DarwinAmd64Url, updateInfo.DarwinAmd64AttestationUrl, updateInfo.DarwinArm64Url, updateInfo.DarwinArm64AttestationUrl, updateInfo.DarwinAmd64Xattr, updateInfo.DarwinAmd64Xattr}
|
urls := []string{updateInfo.LinuxAmd64Url, updateInfo.LinuxAmd64AttestationUrl,
|
||||||
|
updateInfo.DarwinAmd64Url, updateInfo.DarwinAmd64UnsignedUrl, updateInfo.DarwinAmd64AttestationUrl,
|
||||||
|
updateInfo.DarwinArm64Url, updateInfo.DarwinArm64UnsignedUrl, updateInfo.DarwinArm64AttestationUrl}
|
||||||
for _, url := range urls {
|
for _, url := range urls {
|
||||||
resp, err := http.Get(url)
|
resp, err := http.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -373,11 +375,11 @@ func buildUpdateInfo(version string) shared.UpdateInfo {
|
|||||||
LinuxAmd64Url: fmt.Sprintf("https://github.com/ddworken/hishtory/releases/download/%s-linux-amd64/hishtory-linux-amd64", version),
|
LinuxAmd64Url: fmt.Sprintf("https://github.com/ddworken/hishtory/releases/download/%s-linux-amd64/hishtory-linux-amd64", version),
|
||||||
LinuxAmd64AttestationUrl: fmt.Sprintf("https://github.com/ddworken/hishtory/releases/download/%s-linux-amd64/hishtory-linux-amd64.intoto.jsonl", version),
|
LinuxAmd64AttestationUrl: fmt.Sprintf("https://github.com/ddworken/hishtory/releases/download/%s-linux-amd64/hishtory-linux-amd64.intoto.jsonl", version),
|
||||||
DarwinAmd64Url: fmt.Sprintf("https://github.com/ddworken/hishtory/releases/download/%s-darwin-amd64/hishtory-darwin-amd64", version),
|
DarwinAmd64Url: fmt.Sprintf("https://github.com/ddworken/hishtory/releases/download/%s-darwin-amd64/hishtory-darwin-amd64", version),
|
||||||
|
DarwinAmd64UnsignedUrl: fmt.Sprintf("https://github.com/ddworken/hishtory/releases/download/%s-darwin-amd64/hishtory-darwin-amd64-unsigned", version),
|
||||||
DarwinAmd64AttestationUrl: fmt.Sprintf("https://github.com/ddworken/hishtory/releases/download/%s-darwin-amd64/hishtory-darwin-amd64.intoto.jsonl", version),
|
DarwinAmd64AttestationUrl: fmt.Sprintf("https://github.com/ddworken/hishtory/releases/download/%s-darwin-amd64/hishtory-darwin-amd64.intoto.jsonl", version),
|
||||||
DarwinAmd64Xattr: fmt.Sprintf("https://github.com/ddworken/hishtory/releases/download/%s-xattr/hishtory-darwin-amd64-xattr.json", version),
|
|
||||||
DarwinArm64Url: fmt.Sprintf("https://github.com/ddworken/hishtory/releases/download/%s-darwin-arm64/hishtory-darwin-arm64", version),
|
DarwinArm64Url: fmt.Sprintf("https://github.com/ddworken/hishtory/releases/download/%s-darwin-arm64/hishtory-darwin-arm64", version),
|
||||||
|
DarwinArm64UnsignedUrl: fmt.Sprintf("https://github.com/ddworken/hishtory/releases/download/%s-darwin-arm64/hishtory-darwin-arm64-unsigned", version),
|
||||||
DarwinArm64AttestationUrl: fmt.Sprintf("https://github.com/ddworken/hishtory/releases/download/%s-darwin-arm64/hishtory-darwin-arm64.intoto.jsonl", version),
|
DarwinArm64AttestationUrl: fmt.Sprintf("https://github.com/ddworken/hishtory/releases/download/%s-darwin-arm64/hishtory-darwin-arm64.intoto.jsonl", version),
|
||||||
DarwinArm64Xattr: fmt.Sprintf("https://github.com/ddworken/hishtory/releases/download/%s-xattr/hishtory-darwin-arm64-xattr.json", version),
|
|
||||||
Version: version,
|
Version: version,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,6 @@ package lib
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -23,8 +22,6 @@ import (
|
|||||||
|
|
||||||
_ "embed" // for embedding config.sh
|
_ "embed" // for embedding config.sh
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
|
|
||||||
"github.com/glebarez/sqlite" // an alternate non-cgo-requiring sqlite driver
|
"github.com/glebarez/sqlite" // an alternate non-cgo-requiring sqlite driver
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/logger"
|
"gorm.io/gorm/logger"
|
||||||
@@ -532,7 +529,11 @@ func Update() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify the SLSA attestation
|
// Verify the SLSA attestation
|
||||||
|
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)
|
err = verifyBinary("/tmp/hishtory-client", "/tmp/hishtory-client.intoto.jsonl", downloadData.Version+"-"+runtime.GOOS+"-"+runtime.GOARCH)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to verify SLSA provenance of the updated binary, aborting update: %v", err)
|
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
|
// Install the new one
|
||||||
cmd := exec.Command("chmod", "+x", "/tmp/hishtory-client")
|
cmd := exec.Command("chmod", "+x", "/tmp/hishtory-client")
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
@@ -578,6 +571,98 @@ func Update() error {
|
|||||||
return nil
|
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 {
|
func downloadFiles(updateInfo shared.UpdateInfo) error {
|
||||||
clientUrl := ""
|
clientUrl := ""
|
||||||
clientProvenanceUrl := ""
|
clientProvenanceUrl := ""
|
||||||
@@ -734,79 +819,6 @@ type darwinCodeSignature struct {
|
|||||||
Cs string `json:"cs"`
|
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 {
|
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")
|
return strings.Contains(err.Error(), "dial tcp: lookup api.hishtory.dev") || strings.Contains(err.Error(), "read: connection reset by peer")
|
||||||
}
|
}
|
||||||
|
@@ -71,23 +71,35 @@ func verify(provenance []byte, artifactHash, source, branch, versionTag string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func verifyBinary(binaryPath, attestationPath, versionTag string) error {
|
func verifyBinary(binaryPath, attestationPath, versionTag string) error {
|
||||||
|
if os.Getenv("HISHTORY_DISABLE_SLSA_ATTESTATION") == "true" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Also verify that the version is newer and this isn't a downgrade
|
// TODO: Also verify that the version is newer and this isn't a downgrade
|
||||||
attestation, err := os.ReadFile(attestationPath)
|
attestation, err := os.ReadFile(attestationPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to read attestation file: %v", err)
|
return fmt.Errorf("failed to read attestation file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hash, err := getFileHash(binaryPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return verify(attestation, hash, "github.com/ddworken/hishtory", "master", versionTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFileHash(binaryPath string) (string, error) {
|
||||||
binaryFile, err := os.Open(binaryPath)
|
binaryFile, err := os.Open(binaryPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to read binary for verification purposes: %v", err)
|
return "", fmt.Errorf("failed to read binary for verification purposes: %v", err)
|
||||||
}
|
}
|
||||||
defer binaryFile.Close()
|
defer binaryFile.Close()
|
||||||
|
|
||||||
hasher := sha256.New()
|
hasher := sha256.New()
|
||||||
if _, err := io.Copy(hasher, binaryFile); err != nil {
|
if _, err := io.Copy(hasher, binaryFile); err != nil {
|
||||||
return fmt.Errorf("failed to hash binary: %v", err)
|
return "", fmt.Errorf("failed to hash binary: %v", err)
|
||||||
}
|
}
|
||||||
hash := hex.EncodeToString(hasher.Sum(nil))
|
hash := hex.EncodeToString(hasher.Sum(nil))
|
||||||
|
return hash, nil
|
||||||
return verify(attestation, hash, "github.com/ddworken/hishtory", "master", versionTag)
|
|
||||||
}
|
}
|
||||||
|
@@ -35,11 +35,11 @@ type UpdateInfo struct {
|
|||||||
LinuxAmd64Url string `json:"linux_amd_64_url"`
|
LinuxAmd64Url string `json:"linux_amd_64_url"`
|
||||||
LinuxAmd64AttestationUrl string `json:"linux_amd_64_attestation_url"`
|
LinuxAmd64AttestationUrl string `json:"linux_amd_64_attestation_url"`
|
||||||
DarwinAmd64Url string `json:"darwin_amd_64_url"`
|
DarwinAmd64Url string `json:"darwin_amd_64_url"`
|
||||||
|
DarwinAmd64UnsignedUrl string `json:"darwin_amd_64_unsigned_url"`
|
||||||
DarwinAmd64AttestationUrl string `json:"darwin_amd_64_attestation_url"`
|
DarwinAmd64AttestationUrl string `json:"darwin_amd_64_attestation_url"`
|
||||||
DarwinAmd64Xattr string `json:"darwin_amd_64_xattr_url"`
|
|
||||||
DarwinArm64Url string `json:"darwin_arm_64_url"`
|
DarwinArm64Url string `json:"darwin_arm_64_url"`
|
||||||
|
DarwinArm64UnsignedUrl string `json:"darwin_arm_64_unsigned_url"`
|
||||||
DarwinArm64AttestationUrl string `json:"darwin_arm_64_attestation_url"`
|
DarwinArm64AttestationUrl string `json:"darwin_arm_64_attestation_url"`
|
||||||
DarwinArm64Xattr string `json:"darwin_arm_64_xattr_url"`
|
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user