Watch
1
0
Fork
You've already forked RedFlag
0

sec: hash failure now blocks approval — fail closed

Hash registry is the tamper gate; approving without it was fail-open.
Empty hash with no error (no artifact) still passes.
This commit is contained in:
Fimeg 2026-06-11 20:47:12 -04:00
commit 2b1189aaa2

View file

@ -1402,6 +1402,24 @@ func (h *UpdateHandler) ApproveUpdate(c *gin.Context) {
h.recordSupplyChainOverride(update, targetVersion, hold, req.OverrideReason)
}
// Layer 1: Hash Registry — compute and store expected_sha256 BEFORE writing the
// approval. The hash registry is the tamper-detection gate; an approval whose
// hash step errored must not stand (fail closed, ETHOS §2).
// Empty hash with no error is legitimate: agent-sourced ecosystems (dnf/apt)
// report their closure hashes via ReportDependencies, so computeAndStorePackageHash
// returns ("", nil) for those — approval proceeds normally.
artifactHash, err := h.computeAndStorePackageHash(update, targetVersion)
if err != nil {
log.Printf("[ERROR] [server] [updates] hash_computation_failed id=%s pkg=%s error=%v",
id, update.PackageName, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("hash computation failed; approval not written: %v", err)})
return
}
if artifactHash != "" {
log.Printf("[INFO] [server] [updates] hash_stored id=%s pkg=%s sha256=%s",
id, update.PackageName, artifactHash[:16]+"...")
}
// Proceed with approval (sovereignty principle: vulns + warn-mode age findings
// don't block; only "block" enforcement of the age gate does, handled above).
if len(vulns) > 0 || (!ageDecision.Unknown && ageDecision.WarnMessage != "") {
@ -1418,20 +1436,6 @@ func (h *UpdateHandler) ApproveUpdate(c *gin.Context) {
}
}
// Layer 1: Hash Registry — download artifact and compute expected_sha256
// This is the critical security step: we now have a hash to compare against
// when the agent installs, making any tampering detectable.
artifactHash, err := h.computeAndStorePackageHash(update, targetVersion)
if err != nil {
log.Printf("[ERROR] [server] [updates] hash_computation_failed id=%s pkg=%s error=%v",
id, update.PackageName, err)
// Fail-open: hash failure doesn't block approval, but install will fail
// when the agent can't verify the hash.
} else if artifactHash != "" {
log.Printf("[INFO] [server] [updates] hash_stored id=%s pkg=%s sha256=%s",
id, update.PackageName, artifactHash[:16]+"...")
}
// Enrich the version timeline with what this approval learned — OSV posture,
// publish date, artifact hash — so the detail pane and as-of-date resolution see
// it. Best-effort; never blocks an approval that already cleared the gates.
@ -1468,7 +1472,7 @@ func (h *UpdateHandler) ApproveUpdate(c *gin.Context) {
// closure. Single-entry closure today (top-level package + its expected
// hash); transitive resolution lands here later. Best-effort: a minting
// failure is logged at SECURITY but does not roll back an approval that has
// already cleared OSV/age/hash, mirroring the fail-open hash step above.
// already cleared OSV/age/hash.
if h.minter.Enabled() {
if artifactHash == "" {
// Agent-sourced ecosystems (dnf/apt): the closure was
@ -2011,6 +2015,26 @@ func (h *UpdateHandler) ApproveUpdates(c *gin.Context) {
continue
}
// Layer 1: Hash Registry — compute and store expected_sha256 BEFORE writing
// the approval. Fail closed: hash error blocks this item (added to blockedList)
// without aborting the rest of the batch. Empty hash with no error is fine
// (agent-sourced ecosystems report hashes via ReportDependencies).
artifactHash, err := h.computeAndStorePackageHash(update, targetVersion)
if err != nil {
log.Printf("[ERROR] [server] [updates] bulk_hash_computation_failed id=%s pkg=%s error=%v",
id, update.PackageName, err)
blockedList = append(blockedList, blocked{
UpdateID: idStr,
PackageName: update.PackageName,
Reason: fmt.Sprintf("hash computation failed: %v", err),
})
continue
}
if artifactHash != "" {
log.Printf("[INFO] [server] [updates] bulk_hash_stored id=%s pkg=%s sha256=%s",
id, update.PackageName, artifactHash[:16]+"...")
}
needsMeta := len(vulns) > 0 || (!ageDecision.Unknown && ageDecision.WarnMessage != "")
if needsMeta {
if err := h.updateQueries.ApproveUpdateWithVulns(id, "admin", update.Metadata); err != nil {
@ -2032,17 +2056,6 @@ func (h *UpdateHandler) ApproveUpdates(c *gin.Context) {
}
}
approved++
// Layer 1: Hash Registry — compute and store hash for each approved update
artifactHash, err := h.computeAndStorePackageHash(update, targetVersion)
if err != nil {
log.Printf("[ERROR] [server] [updates] bulk_hash_computation_failed id=%s pkg=%s error=%v",
id, update.PackageName, err)
// Fail-open: hash failure doesn't block approval
} else if artifactHash != "" {
log.Printf("[INFO] [server] [updates] bulk_hash_stored id=%s pkg=%s sha256=%s",
id, update.PackageName, artifactHash[:16]+"...")
}
}
response := gin.H{