carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "bytes"
5 "context"
6 "crypto/ed25519"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "reflect"
11 "testing"
12
13 "github.com/carlosframework/platform/internal/hostbox"
14 "github.com/carlosframework/platform/internal/hostevidence"
15 "github.com/carlosframework/platform/internal/hostguard"
16 "github.com/carlosframework/platform/internal/hostops"
17)
18
19type exactEvidenceInventory struct{ StopInventoryReader }
20
21func (i exactEvidenceInventory) ReadExactStopInventory(ctx context.Context, target hostguard.FenceTarget) (StopInventory, error) {
22 return i.ReadStopInventory(ctx, target.Host)
23}
24
25func TestClaimPendingStopRecoveryProducesPublishableSignedReceipt(t *testing.T) {
26 c, req, _ := claimStopFixture(t)
27 seq := 0
28 c.NewID = func() string { seq++; return fmt.Sprintf("id-%d", seq) }
29 originalKernel := c.Kernel
30 interrupted := errors.New("interrupted after fence")
31 c.Kernel = stopKernelFunc(func(context.Context, hostguard.FenceTarget, StopInventory, DrainAcknowledgement) (StopKernelEvidence, error) {
32 return StopKernelEvidence{}, interrupted
33 })
34 if _, err := c.FenceAndStop(t.Context(), req); !errors.Is(err, interrupted) {
35 t.Fatal(err)
36 }
37 pending, err := c.Guard.PendingFences(t.Context())
38 if err != nil || len(pending) != 1 {
39 t.Fatalf("pending = %+v, %v", pending, err)
40 }
41 target := pending[0]
42 c.Inventory = exactEvidenceInventory{c.Inventory}
43 calls := 0
44 c.Kernel = recoveryOnlyKernel{recover: func(ctx context.Context, target hostguard.FenceTarget, inv StopInventory) (StopKernelEvidence, DrainAcknowledgement, error) {
45 calls++
46 ack, err := c.Drainer.DrainClaim(ctx, req, target.FenceID)
47 if err != nil {
48 return StopKernelEvidence{}, ack, err
49 }
50 evidence, err := originalKernel.VerifyStopped(ctx, target, inv, ack)
51 evidence.ObservedBootID = "recovered-boot"
52 return evidence, ack, err
53 }}
54 for _, broken := range []string{"signer", "request-limit"} {
55 without := c
56 if broken == "signer" {
57 without.EvidenceSigner = nil
58 } else {
59 without.RequestLimit = 0
60 }
61 if _, err := without.RecoverStopped(t.Context(), target); !errors.Is(err, ErrClaimStopUnavailable) || calls != 0 {
62 t.Fatalf("recovery without %s: %v, kernel calls=%d", broken, err, calls)
63 }
64 }
65 result, err := c.RecoverStopped(t.Context(), target)
66 if err != nil {
67 t.Fatal(err)
68 }
69 c.Peers = stopPeerFunc(func(context.Context) (StopPeer, error) {
70 return StopPeer{UID: 1001, PID: 43, ProcessStart: "start-43", BootID: "recovered-boot", ControllerID: "next-controller"}, nil
71 })
72 req.Recover = true
73 replayed, err := c.FenceAndStop(t.Context(), req)
74 if err != nil {
75 t.Fatal(err)
76 }
77 if !bytes.Equal(replayed.Bytes, result.Bytes) || replayed.Key != result.Key || calls != 1 {
78 t.Fatal("successor did not publish the exact recovered receipt")
79 }
80 signature, _, err := c.Evidence.Get(t.Context(), result.Key+".sig")
81 if err != nil {
82 t.Fatal(err)
83 }
84 if err := hostevidence.Verify(c.EvidenceSigner.Public().(ed25519.PublicKey), result.Key, result.Bytes, signature); err != nil {
85 t.Fatal(err)
86 }
87 var producer StopReceipt
88 var consumer hostops.StopReceipt
89 if err := json.Unmarshal(result.Bytes, &producer); err != nil {
90 t.Fatal(err)
91 }
92 // The receipt adds exactly one field the unchanged console does not
93 // declare, binding (roster binding R1; hostops' decoder tolerates it).
94 // Every other field is the console's, with the same values.
95 var fields map[string]json.RawMessage
96 if err := json.Unmarshal(result.Bytes, &fields); err != nil {
97 t.Fatal(err)
98 }
99 if string(fields["binding"]) != `"`+req.ExpectedDirectoryRevision+`"` || string(fields["directory_revision"]) != `"7"` {
100 t.Fatalf("recovered receipt stamps: numeral %s binding %s", fields["directory_revision"], fields["binding"])
101 }
102 delete(fields, "binding")
103 rest, err := json.Marshal(fields)
104 if err != nil {
105 t.Fatal(err)
106 }
107 decoder := json.NewDecoder(bytes.NewReader(rest))
108 decoder.DisallowUnknownFields()
109 if err := decoder.Decode(&consumer); err != nil {
110 t.Fatal(err)
111 }
112 if producer.RequestLimitNS != int64(hostbox.RequestLimit) || producer.ClaimETag != target.ClaimETag || producer.ObservedBootID != "recovered-boot" {
113 t.Fatalf("incomplete recovered receipt: %+v", producer)
114 }
115 consumerBytes, err := json.Marshal(consumer)
116 if err != nil {
117 t.Fatal(err)
118 }
119 var got map[string]any
120 var want map[string]any
121 if json.Unmarshal(consumerBytes, &got) != nil || json.Unmarshal(rest, &want) != nil || !reflect.DeepEqual(got, want) {
122 t.Fatal("producer and consumer receipt fields differ")
123 }
124 if _, err := validateTransportStop(req, result); err != nil {
125 t.Fatal(err)
126 }
127}
128