v0.79.0: escrow upload carries restic_pw_sha256 (SLICE 3 auto-confirm, agent third)

HashResticPassword = sha256 hex over the trimmed password (pinned
cross-repo vector). escrowUploadRequest gains restic_pw_sha256,omitempty
— set only when a staged password was sealed into the blob. Contract test
updated; hub mirrors next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-09 23:05:30 +02:00
parent bd9e777f41
commit 301c84d9b5
5 changed files with 65 additions and 4 deletions
+13
View File
@@ -2,7 +2,9 @@ package escrow
import (
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"os"
@@ -53,6 +55,17 @@ func WipeStagedResticPassword() error {
return nil
}
// HashResticPassword is the CANONICAL hasher for the offsite restic repo password (SLICE 3 hub-verified
// escrow auto-confirm): sha256 hex of the TRIMMED password string — exactly the value AttachResticPassword
// seals into the blob and the value the controller uses (both sides TrimSpace their file reads, so the
// trimmed string is the drift-free convention; pinned by the SAME test vector in felhom-agent and
// felhom-controller). The hash of a 256-bit random secret is non-reversible and non-brute-forceable —
// safe to store on the hub and serve in report ACKs; the PASSWORD itself is never logged or served.
func HashResticPassword(pw string) string {
sum := sha256.Sum256([]byte(strings.TrimSpace(pw)))
return hex.EncodeToString(sum[:])
}
// AttachResticPassword injects the offsite restic repo password from the staged 0600 file into the bundle
// when it exists (fork-4 escrow-create auto-inject). Returns whether it attached. The VALUE is validated
// (non-empty) but NEVER logged by callers — log the field NAME only (mirrors AttachWGKey). A missing file
+15
View File
@@ -97,6 +97,21 @@ func TestIdentity_RoundTrip_CarriesResticPassword(t *testing.T) {
}
}
// PINNED CROSS-REPO TEST VECTOR (SLICE 3): the same vector is asserted in felhom-controller — if either
// side drifts (trailing newline, encoding, trim behavior), its half of this test fails and auto-confirm
// can never silently mismatch. Convention: sha256 hex over the TRIMMED password string.
func TestHashResticPassword_PinnedVector(t *testing.T) {
const vector = "cafef00ddeadbeef0123456789abcdef0123456789abcdef0123456789abcdef"
const want = "dbfc02f987e1ac0c91911d5761267089b1144628745a3343e4d96194e43c08e4"
if got := HashResticPassword(vector); got != want {
t.Fatalf("pinned vector drift: got %s want %s", got, want)
}
// trim convention: surrounding whitespace/newlines do not change the hash (both sides trim)
if got := HashResticPassword(" " + vector + "\n"); got != want {
t.Fatalf("whitespace must not change the hash (trim convention), got %s", got)
}
}
// AttachResticPassword: missing file → clean no-attach; staged file → trimmed value attached; empty → error.
func TestAttachResticPassword(t *testing.T) {
b := &IdentityBundle{}