hub R-1254 (P2): the escrow key, its hash and the identity blob in ONE transaction; a bad identity blob is refused before anything is stored. Committed, not deployed.
gates / gates (push) Successful in 13m27s
gates / gates (push) Successful in 13m27s
This commit is contained in:
1 parent
80f1ab3167
commit
dcf4f84cb7
6 files changed
+165
-16
No files matched your search
+16
-15
@@ -1409,10 +1409,23 @@ func (h *Handler) handleHostEscrowPut(w http.ResponseWriter, r *http.Request, pa
|
||||
if createdAt == "" {
|
||||
createdAt = time.Now().UTC().Format(time.RFC3339)
|
||||
}
|
||||
// Slice 10D.1: optionally the IDENTITY escrow blob alongside the K-escrow (opaque — no usable secret hub-side).
|
||||
// R-1254: decoded BEFORE any write (a bad one used to 400 after the new key was already stored) and saved in the
|
||||
// SAME transaction as the key and its hash — all of the new package or all of the old. The directive is retired
|
||||
// (R-105).
|
||||
var idBlob []byte
|
||||
if req.IdentityBlobB64 != "" {
|
||||
b, derr := base64.StdEncoding.DecodeString(req.IdentityBlobB64)
|
||||
if derr != nil || len(b) == 0 {
|
||||
http.Error(w, "Invalid payload: identity_blob_b64 not valid base64", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
idBlob = b
|
||||
}
|
||||
// Store the OPAQUE bytes. No decrypt path exists — the hub cannot open this. Part B (v0.60.0):
|
||||
// when this upload supersedes a DIFFERENT-passphrase old blob, the old one is RETAINED (not
|
||||
// overwritten) so its recovery-code-recoverable history survives (Viktor's data-first ruling).
|
||||
superseded, prevPwSHA, serr := h.store.SaveHostEscrow(pathHostID, blob, req.KeyFingerprint, req.Posture, createdAt, req.ResticPwSHA256)
|
||||
superseded, prevPwSHA, serr := h.store.SaveHostEscrowWithIdentity(pathHostID, blob, idBlob, req.KeyFingerprint, req.Posture, createdAt, req.ResticPwSHA256)
|
||||
if serr != nil {
|
||||
h.logger.Printf("[ERROR] Failed to store escrow for host %s: %v", pathHostID, serr)
|
||||
http.Error(w, "Internal error", http.StatusInternalServerError)
|
||||
@@ -1434,20 +1447,8 @@ func (h *Handler) handleHostEscrowPut(w http.ResponseWriter, r *http.Request, pa
|
||||
h.maybeEmitRepoKeyChanged(host.CustomerID, pathHostID, prevPwSHA, req.ResticPwSHA256, n)
|
||||
}
|
||||
}
|
||||
// Slice 10D.1: optionally store the IDENTITY escrow blob alongside the K-escrow (opaque — no usable secret
|
||||
// hub-side). Additive: a slice-7 upload without it is unchanged. The directive is retired (R-105).
|
||||
if req.IdentityBlobB64 != "" {
|
||||
idBlob, derr := base64.StdEncoding.DecodeString(req.IdentityBlobB64)
|
||||
if derr != nil || len(idBlob) == 0 {
|
||||
http.Error(w, "Invalid payload: identity_blob_b64 not valid base64", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := h.store.SaveHostDRBundle(pathHostID, idBlob); err != nil {
|
||||
h.logger.Printf("[ERROR] Failed to store DR bundle for host %s: %v", pathHostID, err)
|
||||
http.Error(w, "Internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
h.logger.Printf("[INFO] stored DR bundle for host %s (identity %d bytes)", pathHostID, len(idBlob))
|
||||
if idBlob != nil {
|
||||
h.logger.Printf("[INFO] stored DR bundle for host %s (identity %d bytes, same transaction as the key — R-1254)", pathHostID, len(idBlob))
|
||||
}
|
||||
h.logger.Printf("[INFO] stored opaque escrow blob for host %s (%d bytes, posture=%s, fp=%s)",
|
||||
pathHostID, len(blob), req.Posture, req.KeyFingerprint)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
|
||||
)
|
||||
|
||||
// R-1254: an escrow upload with a malformed identity blob is refused BEFORE anything is stored — until this fix the new
|
||||
// key and its hash were committed first and the bad identity answered 400 after, leaving the new hash beside the old
|
||||
// identity package. Through the real endpoint. RED-PROOF (2026-10-24): with the identity decoded after the key save
|
||||
// (the old order), this fails on „the key was stored before the bad identity was refused".
|
||||
func TestR1254_ABadIdentityBlobStoresNothing(t *testing.T) {
|
||||
h, st, _ := newTestHandler(t)
|
||||
st.UpsertHost(&store.Host{HostID: "h1", CustomerID: "c1", APIKey: "HKEY"})
|
||||
if rr := do(h, http.MethodPut, "/hosts/h1/escrow", "HKEY", escrowBodyWithHash([]byte("k1"), "SHA_GEN1", "age-gen1")); rr.Code != http.StatusOK {
|
||||
t.Fatalf("first PUT = %d (%s)", rr.Code, rr.Body.String())
|
||||
}
|
||||
m := map[string]string{
|
||||
"blob_b64": base64.StdEncoding.EncodeToString([]byte("k2")), "key_fingerprint": "ab:cd:ef",
|
||||
"posture": "zero_knowledge", "created_at": "2026-10-24T21:00:00Z", "restic_pw_sha256": "SHA_GEN2",
|
||||
"identity_blob_b64": "%%% not base64 %%%",
|
||||
}
|
||||
body, _ := json.Marshal(m)
|
||||
if rr := do(h, http.MethodPut, "/hosts/h1/escrow", "HKEY", string(body)); rr.Code != http.StatusBadRequest {
|
||||
t.Fatalf("a bad identity blob must be a 400, got %d", rr.Code)
|
||||
}
|
||||
e, _ := st.GetHostEscrow("h1")
|
||||
if e == nil || string(e.Blob) != "k1" || e.ResticPwSHA256 != "SHA_GEN1" {
|
||||
t.Fatalf("the key was stored before the bad identity was refused: %+v", e)
|
||||
}
|
||||
if b, _ := st.GetHostDRBundle("h1"); b == nil || string(b.IdentityBlob) != "age-gen1" {
|
||||
t.Fatalf("the old identity must stay: %+v", b)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user