diff --git a/Makefile b/Makefile index 53c4bb2..6382b21 100644 --- a/Makefile +++ b/Makefile @@ -43,31 +43,31 @@ go-build-release: \ go-build-release-freebsd-arm64 go-build-release-linux-arm64: - CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o bin/newt_linux_arm64 + CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS) -X main.newtPlatform=linux_arm64" -o bin/newt_linux_arm64 go-build-release-linux-arm32-v7: - CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -ldflags "$(LDFLAGS)" -o bin/newt_linux_arm32 + CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -ldflags "$(LDFLAGS) -X main.newtPlatform=linux_arm32" -o bin/newt_linux_arm32 go-build-release-linux-arm32-v6: - CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -ldflags "$(LDFLAGS)" -o bin/newt_linux_arm32v6 + CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -ldflags "$(LDFLAGS) -X main.newtPlatform=linux_arm32v6" -o bin/newt_linux_arm32v6 go-build-release-linux-amd64: - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o bin/newt_linux_amd64 + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS) -X main.newtPlatform=linux_amd64" -o bin/newt_linux_amd64 go-build-release-linux-riscv64: - CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 go build -ldflags "$(LDFLAGS)" -o bin/newt_linux_riscv64 + CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 go build -ldflags "$(LDFLAGS) -X main.newtPlatform=linux_riscv64" -o bin/newt_linux_riscv64 go-build-release-darwin-arm64: - CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o bin/newt_darwin_arm64 + CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "$(LDFLAGS) -X main.newtPlatform=darwin_arm64" -o bin/newt_darwin_arm64 go-build-release-darwin-amd64: - CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o bin/newt_darwin_amd64 + CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "$(LDFLAGS) -X main.newtPlatform=darwin_amd64" -o bin/newt_darwin_amd64 go-build-release-windows-amd64: - CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o bin/newt_windows_amd64.exe + CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "$(LDFLAGS) -X main.newtPlatform=windows_amd64" -o bin/newt_windows_amd64.exe go-build-release-freebsd-amd64: - CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o bin/newt_freebsd_amd64 + CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags "$(LDFLAGS) -X main.newtPlatform=freebsd_amd64" -o bin/newt_freebsd_amd64 go-build-release-freebsd-arm64: - CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o bin/newt_freebsd_arm64 \ No newline at end of file + CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 go build -ldflags "$(LDFLAGS) -X main.newtPlatform=freebsd_arm64" -o bin/newt_freebsd_arm64 \ No newline at end of file diff --git a/main.go b/main.go index 034ab82..b612fb9 100644 --- a/main.go +++ b/main.go @@ -147,18 +147,19 @@ var ( authDaemonEnabled bool authDaemonGenerateRandomPassword bool // Build/version (can be overridden via -ldflags "-X main.newtVersion=...") - newtVersion = "version_replaceme" + newtVersion = "version_replaceme" + newtPlatform = "" // embedded at build time via -X main.newtPlatform=_ // Observability/metrics flags - metricsEnabled bool - otlpEnabled bool - adminAddr string - region string - metricsAsyncBytes bool - pprofEnabled bool - blueprintFile string - provisioningBlueprintFile string - noCloud bool + metricsEnabled bool + otlpEnabled bool + adminAddr string + region string + metricsAsyncBytes bool + pprofEnabled bool + blueprintFile string + provisioningBlueprintFile string + noCloud bool // New mTLS configuration variables tlsClientCert string @@ -676,6 +677,7 @@ func runNewtMain(ctx context.Context) { NewtID: id, Secret: secret, CurrentVersion: newtVersion, + Platform: newtPlatform, TLSConfig: tlsCfg, }); err != nil { logger.Fatal("Auto-update failed: %v", err) @@ -1867,7 +1869,7 @@ persistent_keepalive_interval=5`, util.FixKey(privateKey.String()), util.FixKey( } else { logger.Warn("CLIENTS WILL NOT WORK ON THIS VERSION OF NEWT WITH THIS VERSION OF PANGOLIN, PLEASE UPDATE THE SERVER TO 1.13 OR HIGHER OR DOWNGRADE NEWT") } - + sendBlueprint(client, blueprintFile) if client.WasJustProvisioned() { logger.Info("Provisioning detected – sending provisioning blueprint") diff --git a/updates/selfupdate.go b/updates/selfupdate.go index 751f358..b322c75 100644 --- a/updates/selfupdate.go +++ b/updates/selfupdate.go @@ -3,7 +3,9 @@ package updates import ( "bytes" "context" + "crypto/sha256" "crypto/tls" + "encoding/hex" "encoding/json" "fmt" "io" @@ -26,6 +28,10 @@ type SelfUpdateConfig struct { Secret string // CurrentVersion is the version of the currently running binary. CurrentVersion string + // Platform is the OS+arch string embedded at build time via ldflags + // (e.g. "linux_amd64", "darwin_arm64"). When non-empty it is used + // directly; when empty the value is derived from runtime.GOOS/GOARCH. + Platform string // TLSConfig is an optional TLS configuration for the HTTP client (may be nil). TLSConfig *tls.Config } @@ -33,9 +39,10 @@ type SelfUpdateConfig struct { // versionResponse mirrors the JSON returned by POST /api/v1/auth/newt/version type versionResponse struct { Data struct { - LatestVersion string `json:"latestVersion"` + LatestVersion string `json:"latestVersion"` CurrentIsLatest bool `json:"currentIsLatest"` - DownloadUrl string `json:"downloadUrl"` + DownloadUrl string `json:"downloadUrl"` + Sha256 string `json:"sha256"` } `json:"data"` Success bool `json:"success"` Message string `json:"message"` @@ -51,6 +58,7 @@ func isOfficialContainer() bool { // platform returns the OS+arch string used in the newt release binary names, // e.g. "linux_amd64", "darwin_arm64", "windows_amd64". +// It is used as a fallback when no platform was embedded at build time. func platform() string { goarch := runtime.GOARCH goos := runtime.GOOS @@ -71,6 +79,26 @@ func platform() string { return fmt.Sprintf("%s_%s", goos, arch) } +// verifySHA256 checks that the file at path has the expected SHA-256 hex digest. +func verifySHA256(path, expected string) error { + f, err := os.Open(path) + if err != nil { + return fmt.Errorf("failed to open file for hashing: %w", err) + } + defer f.Close() + + h := sha256.New() + if _, err := io.Copy(h, f); err != nil { + return fmt.Errorf("failed to hash file: %w", err) + } + + got := hex.EncodeToString(h.Sum(nil)) + if !strings.EqualFold(got, expected) { + return fmt.Errorf("sha256 mismatch: expected %s, got %s", expected, got) + } + return nil +} + // CheckAndSelfUpdate contacts the pangolin server, checks whether a newer // version of newt is available, downloads it if so, replaces the running // binary on disk, and re-executes from the new binary. @@ -107,10 +135,14 @@ func CheckAndSelfUpdate(cfg SelfUpdateConfig) error { } // --- Step 1: Ask the server for the latest version --- + plat := cfg.Platform + if plat == "" { + plat = platform() + } reqBody, err := json.Marshal(map[string]string{ "newtId": cfg.NewtID, "secret": cfg.Secret, - "platform": platform(), + "platform": plat, }) if err != nil { return fmt.Errorf("failed to marshal version request: %w", err) @@ -159,6 +191,16 @@ func CheckAndSelfUpdate(cfg SelfUpdateConfig) error { fmt.Printf("Update available: %s → %s\n", cfg.CurrentVersion, verResp.Data.LatestVersion) fmt.Printf("Downloading from: %s\n", verResp.Data.DownloadUrl) + // --- Pre-download: verify we can write to the binary's directory --- + // Do this before downloading so a permission failure doesn't waste bandwidth. + exeDir := filepath.Dir(exePath) + writeTestFile, err := os.CreateTemp(exeDir, ".newt-write-test-*") + if err != nil { + return fmt.Errorf("cannot write to %s (you may need to run as root or with elevated permissions): %w", exeDir, err) + } + writeTestFile.Close() + _ = os.Remove(writeTestFile.Name()) + // --- Step 2: Download the new binary --- dlCtx, dlCancel := context.WithTimeout(context.Background(), 5*time.Minute) defer dlCancel() @@ -180,11 +222,7 @@ func CheckAndSelfUpdate(cfg SelfUpdateConfig) error { // Write to a temp file in the same directory as the current binary so that // an atomic rename works even across filesystem boundaries. - exeDir := filepath.Dir(exePath) tmpFile, err := os.CreateTemp(exeDir, ".newt-update-*") - if err != nil { - return fmt.Errorf("failed to create temp file for download: %w", err) - } tmpPath := tmpFile.Name() // Ensure the temp file is cleaned up on any error path. @@ -200,6 +238,17 @@ func CheckAndSelfUpdate(cfg SelfUpdateConfig) error { return fmt.Errorf("failed to close temp file: %w", err) } + // --- Verify SHA256 checksum if the server provided one --- + if verResp.Data.Sha256 != "" { + fmt.Println("Verifying SHA256 checksum...") + if err := verifySHA256(tmpPath, verResp.Data.Sha256); err != nil { + return fmt.Errorf("binary integrity check failed: %w", err) + } + fmt.Println("SHA256 checksum verified.") + } else { + fmt.Println("Warning: no SHA256 checksum provided by server, skipping verification.") + } + // Make the new binary executable. if err := os.Chmod(tmpPath, 0755); err != nil { return fmt.Errorf("failed to set executable permission: %w", err)