R-1254 (ceremony half, HUNT-3 H2): after a failed escrow upload, ask the hub which package it holds before wording the result
gates / gates (push) Successful in 44s
gates / gates (push) Successful in 44s
The hub holds the new package -> success; another package -> 502 'previous code still works' (now true); cannot be asked -> 504, no promise. Tests TestR1254_* red before the fix. Commit only; the agent release needs the vouch.
This commit is contained in:
1 parent
3055bc54c1
commit
5ddd124cfc
6 files changed
+294
-5
No files matched your search
@@ -0,0 +1,48 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/hub"
|
||||
)
|
||||
|
||||
// R-1254 H2, the production question: does the hub's current package equal the pending upload body? Compared by the
|
||||
// identity blob the hub serves back. A package without an identity, or a hub that cannot answer, is never „held".
|
||||
// RED-PROOF: make hubHoldsEscrowPackage return true when the hub has any package → the „another package" case
|
||||
// passes as held → FAILS.
|
||||
func TestR1254_HubHoldsComparesTheIdentityBlob(t *testing.T) {
|
||||
body := func(id string) []byte {
|
||||
b, _ := json.Marshal(escrowUploadRequest{BlobB64: base64.StdEncoding.EncodeToString([]byte("k")),
|
||||
IdentityBlobB64: base64.StdEncoding.EncodeToString([]byte(id))})
|
||||
return b
|
||||
}
|
||||
hubWith := func(id string, present bool, err error) func(context.Context) (*hub.IdentityEscrowResponse, error) {
|
||||
return func(context.Context) (*hub.IdentityEscrowResponse, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &hub.IdentityEscrowResponse{Present: present, IdentityEscrowB64: base64.StdEncoding.EncodeToString([]byte(id))}, nil
|
||||
}
|
||||
}
|
||||
ctx := context.Background()
|
||||
if held, err := hubHoldsEscrowPackage(ctx, hubWith("NEW", true, nil), body("NEW")); err != nil || !held {
|
||||
t.Fatalf("the hub holds the new package: held=%v err=%v", held, err)
|
||||
}
|
||||
if held, err := hubHoldsEscrowPackage(ctx, hubWith("OLD", true, nil), body("NEW")); err != nil || held {
|
||||
t.Fatalf("the hub holds another package: held=%v err=%v", held, err)
|
||||
}
|
||||
if held, err := hubHoldsEscrowPackage(ctx, hubWith("", false, nil), body("NEW")); err != nil || held {
|
||||
t.Fatalf("the hub holds nothing: held=%v err=%v", held, err)
|
||||
}
|
||||
if _, err := hubHoldsEscrowPackage(ctx, hubWith("", false, errors.New("unreachable")), body("NEW")); err == nil {
|
||||
t.Fatal("a hub that cannot answer must be an error (unknown), never „not held\"")
|
||||
}
|
||||
noID, _ := json.Marshal(escrowUploadRequest{BlobB64: "aw=="})
|
||||
if _, err := hubHoldsEscrowPackage(ctx, hubWith("NEW", true, nil), noID); err == nil {
|
||||
t.Fatal("a package without an identity blob cannot be compared — an error")
|
||||
}
|
||||
}
|
||||
@@ -1250,6 +1250,14 @@ func runDaemon(cfg config.Config, logger *slog.Logger, logRing *applog.Ring) int
|
||||
// R-978: the ceremony hands its package back; the claim uploads it with the same PUT the root
|
||||
// subprocess used, under the same per-host key, before the code is handed out.
|
||||
Upload: func(ctx context.Context, pkg []byte) error { return putEscrowBody(ctx, cfg, pkg) },
|
||||
// R-1254 H2: after a failed upload, the hub is asked which package it holds before the household is told
|
||||
// anything — the same per-host GET the identity read-back uses.
|
||||
HubHolds: func(ctx context.Context, pkg []byte) (bool, error) {
|
||||
if client == nil {
|
||||
return false, fmt.Errorf("hub client not configured")
|
||||
}
|
||||
return hubHoldsEscrowPackage(ctx, client.FetchIdentityEscrow, pkg)
|
||||
},
|
||||
DRConfigured: func() bool {
|
||||
if drConfigured != nil {
|
||||
return drConfigured()
|
||||
@@ -3366,6 +3374,32 @@ func escrowUploadBody(res escrow.CreateResult, resticPwSHA256 string) []byte {
|
||||
|
||||
// putEscrowBody PUTs an escrow upload body to this host's hub record (the per-host key). Used by the
|
||||
// text-mode ceremony at once, and by the daemon when the customer claims the code (R-978).
|
||||
// hubHoldsEscrowPackage (R-1254 H2) reports whether the hub's CURRENT package is the upload body pkg, by its identity
|
||||
// blob — the part the hub serves back (GET /api/v1/hosts/<id>/escrow). Ciphertext only; nothing is logged. A package
|
||||
// without an identity blob cannot be compared: an error (the caller then promises nothing).
|
||||
func hubHoldsEscrowPackage(ctx context.Context, fetch func(context.Context) (*hub.IdentityEscrowResponse, error), pkg []byte) (bool, error) {
|
||||
var up escrowUploadRequest
|
||||
if err := json.Unmarshal(pkg, &up); err != nil {
|
||||
return false, fmt.Errorf("reading the pending package: %w", err)
|
||||
}
|
||||
mine, err := base64.StdEncoding.DecodeString(up.IdentityBlobB64)
|
||||
if err != nil || len(mine) == 0 {
|
||||
return false, fmt.Errorf("the pending package carries no identity blob to compare")
|
||||
}
|
||||
resp, err := fetch(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !resp.Present || resp.IdentityEscrowB64 == "" {
|
||||
return false, nil
|
||||
}
|
||||
theirs, err := base64.StdEncoding.DecodeString(resp.IdentityEscrowB64)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("decoding the hub's package: %w", err)
|
||||
}
|
||||
return bytes.Equal(mine, theirs), nil
|
||||
}
|
||||
|
||||
func putEscrowBody(ctx context.Context, cfg config.Config, body []byte) error {
|
||||
if cfg.Hub.URL == "" || cfg.Hub.HostID == "" || cfg.Hub.APIKey == "" {
|
||||
return fmt.Errorf("hub not configured (url/host_id/api_key)")
|
||||
|
||||
Reference in new issue
Block a user