package release // The host-retry log (readiness round 2, Task 3; Astra plan 9): signed // `carlos instances retry` grants ride their own object, which no released // console reads or rewrites, so an older console that re-marshals the // restart-scope log cannot strip a grant's signature, and an older box // never sees one. import ( "context" "encoding/json" "strings" "testing" "time" ) func TestHostRetryKeyIsItsOwnObjectUnderTheAppPrefix(t *testing.T) { if got, want := HostRetryKey("bab", "titogo"), "apps/bab/titogo/host-retry.json"; got != want { t.Fatalf("HostRetryKey = %q, want %q", got, want) } if HostRetryKey("bab", "titogo") == RestartScopeKey("bab", "titogo") || HostRetryKey("bab", "titogo") == RestartGenKey("bab", "titogo") { t.Fatal("the host-retry log shares a key with a restart signal") } } func TestAppendHostRetryRoundTrip(t *testing.T) { s := NewDirStore(t.TempDir()) ctx := context.Background() pub, priv := hostRetryKey(t) if g, found, err := LoadHostRetries(ctx, s, "bab", "titogo"); err != nil || found || g == nil || g.Schema != 1 || len(g.Requests) != 0 { t.Fatalf("absent log = %+v, %v, %v; want an empty schema-1 log, not found", g, found, err) } wire, err := SignHostRetry(RootSigner(priv), "bab", "titogo", "a.example.com", "v2", "book-1", 9, time.Now()) if err != nil { t.Fatal(err) } first, err := AppendHostRetry(ctx, s, "bab", "titogo", "a.example.com", wire) if err != nil { t.Fatalf("append: %v", err) } second, err := AppendHostRetry(ctx, s, "bab", "titogo", "b.example.com", wire) if err != nil { t.Fatalf("second append: %v", err) } if first.ID == "" || first.ID == second.ID { t.Fatalf("ids %q and %q: want two distinct requests", first.ID, second.ID) } if _, err := time.Parse(time.RFC3339, first.RequestedAt); err != nil { t.Fatalf("RequestedAt %q: %v", first.RequestedAt, err) } g, found, err := LoadHostRetries(ctx, s, "bab", "titogo") if err != nil || !found { t.Fatalf("load = %v, found %v", err, found) } if g.Schema != 1 || len(g.Requests) != 2 || g.Requests[0].ID != first.ID || g.Requests[1].ID != second.ID { t.Fatalf("log = %+v, want both requests, oldest first", g) } r := g.Requests[0] if r.Scope.Kind != RestartScopeRetry || r.Scope.Value != "a.example.com" || r.Scope.Signed != wire { t.Fatalf("entry = %+v", r) } if _, err := VerifyHostRetry(pub, r.Scope.Signed, "bab", "titogo", "a.example.com", time.Now()); err != nil { t.Fatalf("the stored grant does not verify: %v", err) } // The same JSON shape as the restart-scope log. raw, _, err := s.Get(ctx, HostRetryKey("bab", "titogo")) if err != nil { t.Fatal(err) } var shape struct { Schema int `json:"schema"` Requests []struct { ID string `json:"id"` RequestedAt string `json:"requested_at"` Scope struct { Kind, Value, Signed string } `json:"scope"` } `json:"requests"` } if err := json.Unmarshal(raw, &shape); err != nil || shape.Schema != 1 || len(shape.Requests) != 2 || shape.Requests[0].Scope.Signed != wire { t.Fatalf("wire = %s (%v)", raw, err) } // Nothing landed in the restart-scope log. if _, found, _ := LoadRestartScopes(ctx, s, "bab", "titogo"); found { t.Fatal("a host retry wrote the restart-scope log") } } func TestAppendHostRetryRefusesWhatNoBoxCouldTake(t *testing.T) { s := NewDirStore(t.TempDir()) ctx := context.Background() for _, tc := range []struct{ name, acct, app, host, signed string }{ {"unsigned", "bab", "titogo", "a.example.com", ""}, {"no host", "bab", "titogo", "", "x"}, {"bad host", "bab", "titogo", "a\nb", "x"}, {"oversized grant", "bab", "titogo", "a.example.com", strings.Repeat("x", MaxRootEnvelopeBytes+1)}, {"bad account", "../etc", "titogo", "a.example.com", "x"}, {"bad app", "bab", "../x", "a.example.com", "x"}, } { if _, err := AppendHostRetry(ctx, s, tc.acct, tc.app, tc.host, tc.signed); err == nil { t.Errorf("%s: appended", tc.name) } } if _, found, _ := LoadHostRetries(ctx, s, "bab", "titogo"); found { t.Fatal("a refused append wrote the log") } } func TestHostRetryLogIsBoundedAndRetriesACASConflict(t *testing.T) { cs := &conflictOnceStore{Store: NewDirStore(t.TempDir())} ctx := context.Background() if _, err := AppendHostRetry(ctx, cs, "bab", "titogo", "a.example.com", "x"); err != nil || cs.conflicts != 1 { t.Fatalf("append through one conflict = %v (conflicts %d)", err, cs.conflicts) } var last RestartRequest for i := 0; i < restartScopeKeep+5; i++ { r, err := AppendHostRetry(ctx, cs, "bab", "titogo", "a.example.com", "x") if err != nil { t.Fatal(err) } last = r } g, _, err := LoadHostRetries(ctx, cs, "bab", "titogo") if err != nil { t.Fatal(err) } if len(g.Requests) != restartScopeKeep || g.Newest() != last.ID { t.Fatalf("log holds %d (newest %q), want %d ending at %q", len(g.Requests), g.Newest(), restartScopeKeep, last.ID) } } func TestHostRetryLogRefusesANewerSchema(t *testing.T) { s := NewDirStore(t.TempDir()) ctx := context.Background() if _, err := s.Put(ctx, HostRetryKey("bab", "titogo"), []byte(`{"schema":2,"requests":[]}`), ""); err != nil { t.Fatal(err) } if _, _, err := LoadHostRetries(ctx, s, "bab", "titogo"); err == nil { t.Fatal("a schema-2 log parsed") } if _, err := AppendHostRetry(ctx, s, "bab", "titogo", "a.example.com", "x"); err == nil { t.Fatal("appended to a schema-2 log") } } // Recon D ยง2 failure (a): a console built before RestartScope.Signed // re-marshals the whole restart-scope log on every append, and so strips // every pending grant's signature in it. The grants are not in it any // more: such a rewrite leaves the host-retry log byte for byte as it was. func TestARestartScopeRewriteWithoutSignedLeavesHostRetriesAlone(t *testing.T) { s := NewDirStore(t.TempDir()) ctx := context.Background() _, priv := hostRetryKey(t) wire, err := SignHostRetry(RootSigner(priv), "bab", "titogo", "a.example.com", "v2", "book-1", 9, time.Now()) if err != nil { t.Fatal(err) } if _, err := AppendHostRetry(ctx, s, "bab", "titogo", "a.example.com", wire); err != nil { t.Fatal(err) } before, beforeTag, err := s.Get(ctx, HostRetryKey("bab", "titogo")) if err != nil { t.Fatal(err) } if _, err := AppendRestartScope(ctx, s, "bab", "titogo", RestartScope{Kind: RestartScopeHost, Value: "b.example.com"}); err != nil { t.Fatal(err) } // The older console's append: its struct has no Signed. type oldScope struct { Kind string `json:"kind"` Value string `json:"value"` } type oldRequest struct { ID string `json:"id"` RequestedAt string `json:"requested_at"` Scope oldScope `json:"scope"` } var old struct { Schema int `json:"schema"` Requests []oldRequest `json:"requests"` } raw, tag, err := s.Get(ctx, RestartScopeKey("bab", "titogo")) if err != nil { t.Fatal(err) } if err := json.Unmarshal(raw, &old); err != nil { t.Fatal(err) } old.Requests = append(old.Requests, oldRequest{ID: "beef", RequestedAt: time.Now().UTC().Format(time.RFC3339), Scope: oldScope{Kind: RestartScopeHost, Value: "c.example.com"}}) body, _ := json.Marshal(old) if _, err := s.Put(ctx, RestartScopeKey("bab", "titogo"), body, tag); err != nil { t.Fatal(err) } after, afterTag, err := s.Get(ctx, HostRetryKey("bab", "titogo")) if err != nil { t.Fatal(err) } if string(after) != string(before) || afterTag != beforeTag { t.Fatalf("the host-retry log moved under a restart-scope rewrite:\n%s\n%s", before, after) } g, _, err := LoadHostRetries(ctx, s, "bab", "titogo") if err != nil || len(g.Requests) != 1 || g.Requests[0].Scope.Signed != wire { t.Fatalf("host retries = %+v, %v; want the signed grant intact", g, err) } } // No new write can put a retry, or any signature, in the restart-scope log: // grants ride host-retry.json only (AppendHostRetry). A box still reads a // signed retry an older console left there, with the same checks. func TestValidRestartScopeRefusesARetryAndASignature(t *testing.T) { for _, sc := range []RestartScope{ {Kind: RestartScopeRetry, Value: "a.example.com"}, {Kind: RestartScopeRetry, Value: "a.example.com", Signed: `{"payload":"x","sig":"y"}`}, {Kind: RestartScopeHost, Value: "a.example.com", Signed: "x"}, {Kind: RestartScopeEnvironment, Value: "relay", Signed: "x"}, } { if err := ValidRestartScope(sc); err == nil { t.Errorf("ValidRestartScope(%+v) accepted it", sc) } } s := NewDirStore(t.TempDir()) if _, err := AppendRestartScope(context.Background(), s, "bab", "titogo", RestartScope{Kind: RestartScopeRetry, Value: "a.example.com", Signed: "x"}); err == nil { t.Fatal("a retry was appended to the restart-scope log") } }