gate: route desktop updates through the helper
Desktop self-updates ran in the agent process with their own replay file. They now go through the privileged helper like agent and helper self-updates, so the agent performs no binary mutation and keeps no replay state of its own. The desktop app is a status surface only.
This commit is contained in:
parent
c35ad89b92
commit
7d47b0769d
4 changed files with 141 additions and 291 deletions
|
|
@ -2,7 +2,6 @@ package supplychain
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
|
@ -13,10 +12,8 @@ import (
|
|||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Fimeg/RedFlag/agent/internal/capability"
|
||||
"github.com/Fimeg/RedFlag/agent/internal/constants"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -34,22 +31,13 @@ const (
|
|||
// Both must be updated together if this path changes.
|
||||
helperSelfStagingPath = "/var/lib/redflag/agent/pending-helper.bin"
|
||||
|
||||
maxSelfUpdateBinarySize = 500 * 1024 * 1024
|
||||
)
|
||||
// desktopSelfStagingPath is a cross-language contract: the agent stages the
|
||||
// new desktop binary here and the Rust helper reads it on desktop-self
|
||||
// install. The Rust counterpart is DEFAULT_DESKTOP_SELF_SOURCE in
|
||||
// helper/src/main.rs. Both must be updated together if this path changes.
|
||||
desktopSelfStagingPath = "/var/lib/redflag/agent/pending-desktop.bin"
|
||||
|
||||
// Exit codes mirror helper/src/main.rs so capability receipts have one taxonomy
|
||||
// across helper-executed and agent-local self-update paths.
|
||||
const (
|
||||
policyExitOK = 0
|
||||
policyExitBadToken = 10
|
||||
policyExitTimeWindow = 12
|
||||
policyExitKeyNotFound = 14
|
||||
policyExitSignature = 15
|
||||
policyExitArtifact = 16
|
||||
policyExitReplay = 17
|
||||
policyExitUnsupportedOp = 18
|
||||
policyExitExecFailed = 19
|
||||
policyExitInternal = 20
|
||||
maxSelfUpdateBinarySize = 500 * 1024 * 1024
|
||||
)
|
||||
|
||||
func (c *Consumer) processAgentSelfToken(ctx context.Context, token *capability.Token) (*PolicyResult, error) {
|
||||
|
|
@ -98,74 +86,44 @@ func (c *Consumer) processHelperSelfToken(ctx context.Context, token *capability
|
|||
return c.executor.Execute(ctx, token)
|
||||
}
|
||||
|
||||
// processDesktopSelfToken stages the new desktop binary where the helper reads
|
||||
// it, then hands the token to the privileged executor — the same path agent-self
|
||||
// and helper-self take. The helper verifies the signature and hash, consumes the
|
||||
// replay slot (record-before-install), and atomically installs the binary. The
|
||||
// agent no longer mutates the binary or keeps its own replay file (GATE-004): the
|
||||
// desktop app stays a pure status surface, and the one hardened replay guard
|
||||
// lives in the helper. The token, closure, and signed message are unchanged.
|
||||
func (c *Consumer) processDesktopSelfToken(ctx context.Context, token *capability.Token) (*PolicyResult, error) {
|
||||
if err := validateDirectSelfToken(token); err != nil {
|
||||
return selfPolicyResult(token, "denied", "desktop_self_token_invalid", policyExitSignature, 0, err), nil
|
||||
}
|
||||
if token.Operation != selfUpdateOperation {
|
||||
return selfPolicyResult(
|
||||
token,
|
||||
"denied",
|
||||
"operation_not_allowed",
|
||||
policyExitUnsupportedOp,
|
||||
0,
|
||||
fmt.Errorf("operation=%s (desktop-self requires upgrade)", token.Operation),
|
||||
), nil
|
||||
return nil, fmt.Errorf("desktop-self operation %q not supported", token.Operation)
|
||||
}
|
||||
|
||||
// Bound the in-process staging + install to match the executor timeout.
|
||||
// The agent-self and helper-self paths go through executor.Execute which
|
||||
// carries its own context timeout; desktop-self runs in-process and needs
|
||||
// an equivalent guard against hung downloads or stalled I/O.
|
||||
ctx, cancel := context.WithTimeout(ctx, executorTimeout)
|
||||
defer cancel()
|
||||
|
||||
entry, err := findClosureEntry(token, "redflag-desktop")
|
||||
if err != nil {
|
||||
return selfPolicyResult(token, "denied", "desktop_self_no_entry", policyExitBadToken, 0, err), nil
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check context before starting potentially long staging operation.
|
||||
if err := ctx.Err(); err != nil {
|
||||
return selfPolicyResult(token, "failed", "desktop_self_cancelled", policyExitExecFailed, 0, err), nil
|
||||
}
|
||||
|
||||
stagingPath := filepath.Join(constants.GetAgentStateDir(), "desktop-self-upgrade.bin")
|
||||
staged, err := c.stageClosureArtifact(entry, stagingPath)
|
||||
staged, err := c.stageClosureArtifact(entry, desktopSelfStagingPath)
|
||||
if err != nil {
|
||||
return selfPolicyResult(token, "failed", "desktop_self_stage_failed", policyExitArtifact, 0, err), nil
|
||||
return nil, fmt.Errorf("stage desktop-self artifact: %w", err)
|
||||
}
|
||||
defer os.Remove(staged)
|
||||
|
||||
// Check replay before any side effects; record consumed only after a
|
||||
// successful install. The token is Ed25519-signed, hash-pinned, and
|
||||
// version-pinned, so re-executing the identical install after a transient
|
||||
// failure (disk full, backup fail) is idempotent — burning the token before
|
||||
// install would permanently prevent retry (ETHOS #3, #4).
|
||||
if err := replayCheckAgent(token.TokenID); err != nil {
|
||||
return selfPolicyResult(token, "denied", "token_already_consumed", policyExitReplay, 1, err), nil
|
||||
}
|
||||
|
||||
target, err := installDesktopBinary(staged)
|
||||
result, err := c.executor.Execute(ctx, token)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] [agent] [supplychain] desktop_self_install_failed token_id=%s error=%v", token.TokenID, err)
|
||||
return selfPolicyResult(token, "failed", "desktop_self_install_failed", policyExitExecFailed, 1, err), nil
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Install succeeded — mark token consumed so it cannot be replayed.
|
||||
if err := recordAgentTokenConsumed(token.TokenID); err != nil {
|
||||
// Log but do not abort: the binary is already in place. A duplicate
|
||||
// replay would be caught by hash + version checks, and the helper
|
||||
// would refuse an identical re-install anyway.
|
||||
log.Printf("[WARNING] [agent] [supplychain] desktop_self_replay_record_failed token_id=%s error=%v", token.TokenID, err)
|
||||
// Best-effort: nudge the running desktop app to relaunch on the new binary.
|
||||
// This is not a privileged or trust-sensitive step — just a signal — so it
|
||||
// stays agent-side. A failure here does not undo a successful install.
|
||||
if result != nil && result.Executed {
|
||||
if target, perr := desktopBinaryPath(); perr == nil {
|
||||
if sigErr := signalDesktopRestart(target); sigErr != nil {
|
||||
log.Printf("[WARNING] [agent] [supplychain] desktop_restart_signal_failed token_id=%s error=%v", token.TokenID, sigErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := signalDesktopRestart(target); err != nil {
|
||||
log.Printf("[WARNING] [agent] [supplychain] desktop_restart_signal_failed token_id=%s error=%v", token.TokenID, err)
|
||||
}
|
||||
|
||||
log.Printf("[SECURITY] [agent] [supplychain] desktop_self_upgraded token_id=%s path=%s", token.TokenID, target)
|
||||
return selfPolicyResult(token, "executed", "desktop_self_upgraded", policyExitOK, 1, nil), nil
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func findClosureEntry(token *capability.Token, name string) (*capability.ClosureEntry, error) {
|
||||
|
|
@ -266,125 +224,6 @@ func isDownloadRef(ref string) bool {
|
|||
return strings.HasPrefix(ref, "http://") || strings.HasPrefix(ref, "https://") || strings.HasPrefix(ref, "/api/")
|
||||
}
|
||||
|
||||
func validateDirectSelfToken(token *capability.Token) error {
|
||||
if token.Version != capability.Version {
|
||||
return fmt.Errorf("unsupported token version=%d supported=%d", token.Version, capability.Version)
|
||||
}
|
||||
now := time.Now().UTC().Unix()
|
||||
if now < token.NotBefore {
|
||||
return fmt.Errorf("token not yet valid: now=%d not_before=%d", now, token.NotBefore)
|
||||
}
|
||||
if now > token.ExpiresAt {
|
||||
return fmt.Errorf("token expired: now=%d expires_at=%d", now, token.ExpiresAt)
|
||||
}
|
||||
|
||||
pub, err := loadCapabilityPublicKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keyID := capability.KeyIDFor(ed25519.PublicKey(pub))
|
||||
if token.KeyID != keyID {
|
||||
return fmt.Errorf("key_id mismatch: token=%s local=%s", token.KeyID, keyID)
|
||||
}
|
||||
if err := token.Verify(ed25519.PublicKey(pub)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadCapabilityPublicKey() ([]byte, error) {
|
||||
keyPath := constants.GetServerPublicKeyPath()
|
||||
data, err := os.ReadFile(keyPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("public key not found at %s: %w", keyPath, err)
|
||||
}
|
||||
if len(data) == ed25519.PublicKeySize {
|
||||
return data, nil
|
||||
}
|
||||
trimmed := strings.TrimSpace(string(data))
|
||||
decoded, decErr := hex.DecodeString(trimmed)
|
||||
if decErr == nil && len(decoded) == ed25519.PublicKeySize {
|
||||
return decoded, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid public key size at %s: raw=%d decoded=%d", keyPath, len(data), len(decoded))
|
||||
}
|
||||
|
||||
// replayCheckAgent returns an error if tokenID has already been consumed.
|
||||
// It does NOT record the token; call recordAgentTokenConsumed after a
|
||||
// successful install to close the replay window.
|
||||
func replayCheckAgent(tokenID string) error {
|
||||
if _, err := safeTokenFilename(tokenID); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.ContainsAny(tokenID, "\r\n") {
|
||||
return fmt.Errorf("unsafe token_id: contains newline")
|
||||
}
|
||||
|
||||
statePath := filepath.Join(constants.GetAgentStateDir(), "consumed-self-tokens")
|
||||
contents, err := os.ReadFile(statePath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("read replay state: %w", err)
|
||||
}
|
||||
if err == nil {
|
||||
for _, line := range strings.Split(string(contents), "\n") {
|
||||
if strings.TrimSpace(line) == tokenID {
|
||||
return fmt.Errorf("token_id=%s", tokenID)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// recordAgentTokenConsumed appends tokenID to the consumed-self-tokens file.
|
||||
// Call only after a successful install to avoid permanently burning the token
|
||||
// on a transient failure.
|
||||
func recordAgentTokenConsumed(tokenID string) error {
|
||||
statePath := filepath.Join(constants.GetAgentStateDir(), "consumed-self-tokens")
|
||||
if err := os.MkdirAll(filepath.Dir(statePath), 0o700); err != nil {
|
||||
return fmt.Errorf("create replay state dir: %w", err)
|
||||
}
|
||||
f, err := os.OpenFile(statePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open replay state: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
if _, err := fmt.Fprintln(f, tokenID); err != nil {
|
||||
return fmt.Errorf("write replay state: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func installDesktopBinary(staged string) (string, error) {
|
||||
target, err := desktopBinaryPath()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
targetDir := filepath.Dir(target)
|
||||
if info, err := os.Stat(targetDir); err != nil {
|
||||
return "", fmt.Errorf("desktop install dir unavailable: %w", err)
|
||||
} else if !info.IsDir() {
|
||||
return "", fmt.Errorf("desktop install parent is not a directory: %s", targetDir)
|
||||
}
|
||||
|
||||
if info, err := os.Stat(target); err == nil {
|
||||
if info.IsDir() {
|
||||
return "", fmt.Errorf("desktop target is a directory: %s", target)
|
||||
}
|
||||
// One generation back: overwrite stale .bak with current before replacing.
|
||||
os.Remove(target + ".bak")
|
||||
if err := copyRegularFile(target, target+".bak"); err != nil {
|
||||
return "", fmt.Errorf("backup desktop binary: %w", err)
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("stat desktop binary: %w", err)
|
||||
}
|
||||
|
||||
if err := atomicReplaceFile(staged, target, 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return target, nil
|
||||
}
|
||||
|
||||
func desktopBinaryPath() (string, error) {
|
||||
if p := os.Getenv("REDFLAG_DESKTOP_BINARY"); p != "" {
|
||||
return p, nil
|
||||
|
|
@ -418,39 +257,6 @@ func signalDesktopRestart(target string) error {
|
|||
return fmt.Errorf("pkill -x %s failed: %w output=%s", name, err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
|
||||
func atomicReplaceFile(staged, target string, mode os.FileMode) error {
|
||||
tmp, err := os.CreateTemp(filepath.Dir(target), filepath.Base(target)+".new.")
|
||||
if err != nil {
|
||||
return fmt.Errorf("create install temp: %w", err)
|
||||
}
|
||||
tmpPath := tmp.Name()
|
||||
if err := tmp.Close(); err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("close install temp: %w", err)
|
||||
}
|
||||
cleanup := true
|
||||
defer func() {
|
||||
if cleanup {
|
||||
os.Remove(tmpPath)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := copyRegularFile(staged, tmpPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Chmod(tmpPath, mode); err != nil {
|
||||
return fmt.Errorf("chmod install temp: %w", err)
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
_ = os.Remove(target)
|
||||
}
|
||||
if err := os.Rename(tmpPath, target); err != nil {
|
||||
return fmt.Errorf("replace binary: %w", err)
|
||||
}
|
||||
cleanup = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyRegularFile(src, dst string) error {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
|
|
@ -490,22 +296,3 @@ func computeFileSHA256Hex(path string) (string, error) {
|
|||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func selfPolicyResult(token *capability.Token, decision, reason string, exitCode, verifiedArtifacts int, resultErr error) *PolicyResult {
|
||||
var errText string
|
||||
if resultErr != nil {
|
||||
errText = resultErr.Error()
|
||||
}
|
||||
return &PolicyResult{
|
||||
TokenID: token.TokenID,
|
||||
AgentID: token.AgentID,
|
||||
PackageType: token.PackageType,
|
||||
Operation: token.Operation,
|
||||
Decision: decision,
|
||||
Reason: reason,
|
||||
Executed: decision == "executed",
|
||||
VerifiedArtifacts: verifiedArtifacts,
|
||||
ExitCode: exitCode,
|
||||
Error: errText,
|
||||
Timestamp: time.Now().UTC().Unix(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -237,12 +237,13 @@ type Consumer struct {
|
|||
reporter Reporter // optional
|
||||
downloader ArtifactDownloader // optional, inferred from reporter when available
|
||||
|
||||
// mu serializes ProcessToken. The replay-state guards (helper-side, and the
|
||||
// agent-side desktop-self file until GATE-004 B lands) are check-then-act and
|
||||
// not safe under concurrent processing of the same token. The instance lock is
|
||||
// process-level only; today's single caller (loop.go) never overlaps, but a
|
||||
// future second caller — local-API trigger, retry worker — would race without
|
||||
// this. One token at a time is the design invariant; this enforces it.
|
||||
// mu serializes ProcessToken. The helper's replay-state guard is check-then-act
|
||||
// (read-scan-rewrite) and not safe under concurrent processing of the same token;
|
||||
// GATE-004 B removed the agent-side desktop-self replay file, so the helper guard
|
||||
// is now the only one. The instance lock is process-level only; today's single
|
||||
// caller (loop.go) never overlaps, but a future second caller — local-API trigger,
|
||||
// retry worker — would race without this. One token at a time is the design
|
||||
// invariant; this enforces it.
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -136,48 +136,6 @@ func TestStageClosureArtifactFromLocalFile(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestInstallDesktopBinaryBacksUpAndReplaces(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
target := filepath.Join(dir, "redflag-desktop")
|
||||
staged := filepath.Join(dir, "redflag-desktop.new")
|
||||
if err := os.WriteFile(target, []byte("old"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(staged, []byte("new"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("REDFLAG_DESKTOP_BINARY", target)
|
||||
|
||||
gotTarget, err := installDesktopBinary(staged)
|
||||
if err != nil {
|
||||
t.Fatalf("installDesktopBinary returned error: %v", err)
|
||||
}
|
||||
if gotTarget != target {
|
||||
t.Fatalf("target = %q, want %q", gotTarget, target)
|
||||
}
|
||||
got, err := os.ReadFile(target)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got) != "new" {
|
||||
t.Fatalf("installed body = %q, want new", string(got))
|
||||
}
|
||||
backup, err := os.ReadFile(target + ".bak")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(backup) != "old" {
|
||||
t.Fatalf("backup body = %q, want old", string(backup))
|
||||
}
|
||||
info, err := os.Stat(target)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if info.Mode().Perm() != 0o755 {
|
||||
t.Fatalf("target mode = %o, want 755", info.Mode().Perm())
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsumerProcessTokenRejectsWrongAgentID(t *testing.T) {
|
||||
agentID := uuid.Must(uuid.NewV4())
|
||||
executor := NewExecutor("/nonexistent/helper")
|
||||
|
|
|
|||
|
|
@ -72,6 +72,19 @@ const HELPER_SELF_PACKAGE_TYPE: &str = "helper-self";
|
|||
// The Go counterpart is helperSelfStagingPath in agent/internal/supplychain/binary_update.go.
|
||||
// Both must be updated together if this path changes.
|
||||
const DEFAULT_HELPER_SELF_SOURCE: &str = "/var/lib/redflag/agent/pending-helper.bin";
|
||||
// Desktop self-upgrade (package_type "desktop-self"). The desktop binary is a
|
||||
// system binary that lives beside the agent binary, so its install is a
|
||||
// privileged swap like the agent's — it now runs through the helper instead of
|
||||
// in the agent process (GATE-004), reusing the same hash-verify + replay guard.
|
||||
const DESKTOP_SELF_PACKAGE_TYPE: &str = "desktop-self";
|
||||
// DEFAULT_DESKTOP_SELF_SOURCE is a cross-language contract: the Go agent stages
|
||||
// the new desktop binary here before invoking the helper with a desktop-self
|
||||
// token. The Go counterpart is desktopSelfStagingPath in
|
||||
// agent/internal/supplychain/binary_update.go. Both must change together.
|
||||
const DEFAULT_DESKTOP_SELF_SOURCE: &str = "/var/lib/redflag/agent/pending-desktop.bin";
|
||||
// DEFAULT_DESKTOP_BINARY is the install target — beside the agent binary, never
|
||||
// taken from the token. Overridable via REDFLAG_DESKTOP_BINARY.
|
||||
const DEFAULT_DESKTOP_BINARY: &str = "/usr/local/bin/redflag-desktop";
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
struct ClosureEntry {
|
||||
|
|
@ -807,6 +820,27 @@ fn install_helper_binary(staged: &str) -> Result<(), Denial> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
// install_desktop_binary backs up the live desktop binary and installs the
|
||||
// verified staged copy beside the agent binary. The install path is a helper
|
||||
// constant, never taken from the token. Runs only after the replay slot is
|
||||
// consumed (the side effect is irreversible). The desktop app is a one-shot the
|
||||
// agent re-launches after a successful install, so no setcap/service restart is
|
||||
// performed here.
|
||||
fn install_desktop_binary(staged: &str) -> Result<(), Denial> {
|
||||
let install = env_or("REDFLAG_DESKTOP_BINARY", DEFAULT_DESKTOP_BINARY);
|
||||
let backup = format!("{}.bak", install);
|
||||
|
||||
if Path::new(&install).exists() {
|
||||
fs::copy(&install, &backup).map_err(|e| {
|
||||
Denial::new(EXIT_EXEC_FAILED, "desktop_self_backup", format!("{} -> {}: {}", install, backup, e))
|
||||
})?;
|
||||
}
|
||||
atomic_replace_binary(staged, &install, "desktop_self_install")?;
|
||||
let _ = fs::remove_file(staged);
|
||||
log_security(&format!("desktop_self_updated path={}", install));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run(token_file: Option<&str>, helper_file: Option<&str>) -> Result<PolicyResult, Box<(Option<CapabilityToken>, Denial)>> {
|
||||
let token = match token_file {
|
||||
Some(path) => read_token_from_file(path).map_err(|d| Box::new((None, d))),
|
||||
|
|
@ -1022,6 +1056,76 @@ fn run(token_file: Option<&str>, helper_file: Option<&str>) -> Result<PolicyResu
|
|||
});
|
||||
}
|
||||
|
||||
// Desktop self-upgrade (GATE-004): the agent stages the new desktop binary and
|
||||
// invokes the helper with a desktop-self token instead of installing it in the
|
||||
// agent process. Same shape as helper-self: verify the hash against the closure
|
||||
// entry, consume the replay slot (record-before-install), then atomically swap
|
||||
// the desktop binary. The token, closure, and signed message are identical to
|
||||
// the old in-process path — only the executor moved.
|
||||
if token.package_type == DESKTOP_SELF_PACKAGE_TYPE {
|
||||
if token.operation != "upgrade" {
|
||||
return Err(Box::new((
|
||||
Some(token),
|
||||
Denial::new(
|
||||
EXIT_UNSUPPORTED_OP,
|
||||
"operation_not_allowed",
|
||||
"desktop-self requires operation=upgrade",
|
||||
),
|
||||
)));
|
||||
}
|
||||
|
||||
let sha256 = {
|
||||
let entry = token.closure.iter().find(|e| e.name == "redflag-desktop");
|
||||
match entry {
|
||||
Some(e) => e.sha256.clone(),
|
||||
None => {
|
||||
return Err(Box::new((
|
||||
Some(token),
|
||||
Denial::new(EXIT_BAD_TOKEN, "desktop_self_no_entry", "closure missing redflag-desktop entry"),
|
||||
)))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let source = env_or("REDFLAG_DESKTOP_SELF_SOURCE", DEFAULT_DESKTOP_SELF_SOURCE);
|
||||
let staging = env_or("REDFLAG_HELPER_STAGING", DEFAULT_HELPER_STAGING) + ".desktop";
|
||||
|
||||
let staged = match stage_and_verify_binary(&source, &staging, &sha256) {
|
||||
Ok(p) => p,
|
||||
Err(d) => return Err(Box::new((Some(token), d))),
|
||||
};
|
||||
|
||||
let state_path = PathBuf::from(env_or("REDFLAG_HELPER_STATE", DEFAULT_STATE_FILE));
|
||||
if let Err(d) = replay_check_and_record(&token.token_id, &state_path) {
|
||||
let _ = fs::remove_file(&staged);
|
||||
return Err(Box::new((Some(token), d)));
|
||||
}
|
||||
|
||||
log_security(&format!(
|
||||
"authorized desktop_self_upgrade token_id={} agent_id={}",
|
||||
token.token_id, token.agent_id
|
||||
));
|
||||
|
||||
if let Err(d) = install_desktop_binary(&staged) {
|
||||
let _ = fs::remove_file(&staged);
|
||||
return Err(Box::new((Some(token), d)));
|
||||
}
|
||||
|
||||
return Ok(PolicyResult {
|
||||
token_id: token.token_id.clone(),
|
||||
agent_id: token.agent_id.clone(),
|
||||
package_type: token.package_type.clone(),
|
||||
operation: token.operation.clone(),
|
||||
decision: "executed".to_string(),
|
||||
reason: "desktop_self_upgraded".to_string(),
|
||||
executed: true,
|
||||
verified_artifacts: 1,
|
||||
exit_code: EXIT_OK,
|
||||
error: None,
|
||||
timestamp: now_unix(),
|
||||
});
|
||||
}
|
||||
|
||||
// Verify artifact hashes the executor can reach.
|
||||
let verified = match verify_artifacts(&token) {
|
||||
Ok(v) => v,
|
||||
|
|
|
|||
Loading…
Reference in a new issue