refactor(updater): export FileSHA256 and reuse it for the update snapshot (W3-2)

admin's fileSHA256 duplicated VerifyChecksum's hashing body. One exported
helper now serves both the TOCTOU snapshot in handleApplyUpdate and
VerifyChecksum itself.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
J3vb
2026-07-19 09:04:28 +02:00
co-authored by Claude Fable 5
parent 9d44942b04
commit 2eec831d6a
2 changed files with 17 additions and 28 deletions
+1 -21
View File
@@ -2,9 +2,6 @@ package admin
import (
"context"
"crypto/sha256"
"encoding/hex"
"io"
"log/slog"
"net/http"
"os"
@@ -90,7 +87,7 @@ func handleApplyUpdate(u *updater.Updater, hub HubBroadcaster, _ string) http.Ha
// Snapshot the hash of the just-verified staged binary. It is re-checked
// immediately before rename+spawn to close the TOCTOU window between
// verification here and the swap in the background goroutine below.
stagedHash, err := fileSHA256(newPath)
stagedHash, err := updater.FileSHA256(newPath)
if err != nil {
slog.Error("update: failed to hash staged binary", "err", err)
_ = os.Remove(newPath)
@@ -159,20 +156,3 @@ func handleApplyUpdate(u *updater.Updater, hub HubBroadcaster, _ string) http.Ha
}()
})
}
// fileSHA256 returns the hex-encoded SHA256 of the file at path. Used to
// snapshot a verified update binary so it can be re-checked (via
// updater.VerifyChecksum) immediately before it is renamed and executed.
func fileSHA256(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close() //nolint:errcheck
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
+16 -7
View File
@@ -615,21 +615,30 @@ func assetFilenameFromURL(rawURL string) (string, error) {
return filename, nil
}
// VerifyChecksum computes the SHA256 hash of the file at filePath and
// compares it (case-insensitive) against expectedHash.
func (u *Updater) VerifyChecksum(filePath, expectedHash string) error {
f, err := os.Open(filePath)
// FileSHA256 returns the hex-encoded SHA256 of the file at path. Exported so
// callers that snapshot a verified binary (the admin update TOCTOU re-check)
// share this exact hashing instead of duplicating it.
func FileSHA256(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("opening file for checksum: %w", err)
return "", fmt.Errorf("opening file for checksum: %w", err)
}
defer f.Close() //nolint:errcheck
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return fmt.Errorf("computing checksum: %w", err)
return "", fmt.Errorf("computing checksum: %w", err)
}
return hex.EncodeToString(h.Sum(nil)), nil
}
actual := hex.EncodeToString(h.Sum(nil))
// VerifyChecksum computes the SHA256 hash of the file at filePath and
// compares it (case-insensitive) against expectedHash.
func (u *Updater) VerifyChecksum(filePath, expectedHash string) error {
actual, err := FileSHA256(filePath)
if err != nil {
return err
}
if !strings.EqualFold(actual, expectedHash) {
return fmt.Errorf("checksum mismatch: expected %s, got %s", expectedHash, actual)
}