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:
parent
6a4be4d12f
commit
2b1189aaa2
1 changed files with 39 additions and 26 deletions
|
|
@ -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{
|
||||
|
|
|
|||
Loading…
Reference in a new issue