Skip to content

Commit f674802

Browse files
authored
[fix] Ad-hoc codesign macOS binaries in rimba update (#26)
fix(updater): add code-signing for macOS binary updates On macOS, downloaded binaries carry quarantine attributes and lack proper code signatures, causing the OS kernel to kill them during execution on Apple Silicon. This change adds platform-specific binary preparation that removes quarantine attributes and applies ad-hoc code signing to ensure updated binaries can execute successfully.
1 parent a397244 commit f674802

5 files changed

Lines changed: 129 additions & 0 deletions

File tree

cmd/update.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ var updateCmd = &cobra.Command{
5858
}
5959
defer updater.CleanupTempDir(newBinary)
6060

61+
if err := updater.PrepareBinary(newBinary); err != nil {
62+
return fmt.Errorf("preparing binary: %w", err)
63+
}
64+
6165
currentBinary, err := os.Executable()
6266
if err != nil {
6367
return fmt.Errorf("locating current binary: %w", err)

internal/updater/prepare_darwin.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//go:build darwin
2+
3+
package updater
4+
5+
import (
6+
"fmt"
7+
"os/exec"
8+
)
9+
10+
// PrepareBinary removes the macOS quarantine attribute and ad-hoc code-signs
11+
// the binary so it can execute on Apple Silicon without being killed.
12+
func PrepareBinary(path string) error {
13+
// Best-effort: remove quarantine attribute (may not exist).
14+
_ = exec.Command("xattr", "-d", "com.apple.quarantine", path).Run() //nolint:gosec // path from controlled temp dir
15+
16+
// Ad-hoc sign — fatal on failure.
17+
if err := exec.Command("codesign", "--sign", "-", "--force", path).Run(); err != nil { //nolint:gosec // path from controlled temp dir
18+
return fmt.Errorf("code-signing binary: %w", err)
19+
}
20+
21+
return nil
22+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//go:build darwin
2+
3+
package updater
4+
5+
import (
6+
"os"
7+
"os/exec"
8+
"path/filepath"
9+
"strings"
10+
"testing"
11+
)
12+
13+
// buildHelperBinary compiles a minimal Go binary for testing.
14+
func buildHelperBinary(t *testing.T, dir string) string {
15+
t.Helper()
16+
17+
src := filepath.Join(dir, "main.go")
18+
if err := os.WriteFile(src, []byte("package main\nfunc main() {}\n"), 0644); err != nil {
19+
t.Fatal(err)
20+
}
21+
22+
bin := filepath.Join(dir, "helper")
23+
cmd := exec.Command("go", "build", "-o", bin, src)
24+
cmd.Env = append(os.Environ(), "CGO_ENABLED=0")
25+
if out, err := cmd.CombinedOutput(); err != nil {
26+
t.Fatalf("building helper binary: %v\n%s", err, out)
27+
}
28+
29+
return bin
30+
}
31+
32+
func TestPrepareBinarySignsExecutable(t *testing.T) {
33+
dir := t.TempDir()
34+
bin := buildHelperBinary(t, dir)
35+
36+
// Strip existing signature so we start unsigned.
37+
if err := exec.Command("codesign", "--remove-signature", bin).Run(); err != nil {
38+
t.Fatalf("removing signature: %v", err)
39+
}
40+
41+
if err := PrepareBinary(bin); err != nil {
42+
t.Fatalf("PrepareBinary: %v", err)
43+
}
44+
45+
// Verify the binary is now validly signed.
46+
if err := exec.Command("codesign", "--verify", bin).Run(); err != nil {
47+
t.Errorf("codesign --verify failed after PrepareBinary: %v", err)
48+
}
49+
}
50+
51+
func TestPrepareBinaryWithQuarantineAttribute(t *testing.T) {
52+
dir := t.TempDir()
53+
bin := buildHelperBinary(t, dir)
54+
55+
// Set the quarantine attribute.
56+
if err := exec.Command("xattr", "-w", "com.apple.quarantine", "0081;00000000;test;", bin).Run(); err != nil {
57+
t.Fatalf("setting quarantine xattr: %v", err)
58+
}
59+
60+
if err := PrepareBinary(bin); err != nil {
61+
t.Fatalf("PrepareBinary: %v", err)
62+
}
63+
64+
// Verify quarantine attribute is removed.
65+
out, err := exec.Command("xattr", "-l", bin).CombinedOutput()
66+
if err != nil {
67+
// xattr -l returns error when no attributes exist — that's fine.
68+
return
69+
}
70+
if strings.Contains(string(out), "com.apple.quarantine") {
71+
t.Error("quarantine attribute still present after PrepareBinary")
72+
}
73+
}
74+
75+
func TestPrepareBinaryInvalidPath(t *testing.T) {
76+
err := PrepareBinary("/nonexistent/path/binary")
77+
if err == nil {
78+
t.Fatal("expected error for nonexistent path")
79+
}
80+
if !strings.Contains(err.Error(), "code-signing binary") {
81+
t.Errorf("error = %q, want to contain 'code-signing binary'", err.Error())
82+
}
83+
}

internal/updater/prepare_other.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
//go:build !darwin
2+
3+
package updater
4+
5+
// PrepareBinary is a no-op on non-darwin platforms.
6+
func PrepareBinary(_ string) error {
7+
return nil
8+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//go:build !darwin
2+
3+
package updater
4+
5+
import "testing"
6+
7+
func TestPrepareBinaryNoOp(t *testing.T) {
8+
// On non-darwin platforms, PrepareBinary should always return nil.
9+
if err := PrepareBinary("/any/path"); err != nil {
10+
t.Errorf("PrepareBinary() = %v, want nil", err)
11+
}
12+
}

0 commit comments

Comments
 (0)