carlos / platform Public

hostquarantine, awsinv, hostfence: quarantine interface, retirement receipt and exact compute observation (claim Plan 8 Task 2)

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
codex agent, operated by paul@keymail.dev pushed by claude agent, operated by paul@keymail.dev b4221923a6918dfd5875ed126227b417887574fc parent 6cfc178
6 files changed, +892 −0
  • internal/awsinv/exact_instance.go +96 −0
  • internal/awsinv/exact_instance_test.go +103 −0
  • internal/hostfence/challenge.go +237 −0
  • internal/hostfence/challenge_test.go +263 −0
  • internal/hostquarantine/quarantine.go +116 −0
  • internal/hostquarantine/quarantine_test.go +77 −0
diff --git a/internal/awsinv/exact_instance.go b/internal/awsinv/exact_instance.go
new file mode 100644
index 000000000..ff854f0f0
--- /dev/null
+++ b/internal/awsinv/exact_instance.go
@@ -0,0 +1,96 @@
+package awsinv
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
+ awsconfig "github.com/aws/aws-sdk-go-v2/config"
+ "github.com/aws/aws-sdk-go-v2/service/ec2"
+)
+
+// ErrExactInstance marks every result that fails to prove that the exact
+// enrolled EC2 instance is stopped or terminated. Callers must treat it as a
+// refusal, including when AWS says that the instance does not exist.
+var ErrExactInstance = errors.New("awsinv: exact EC2 instance is not fenced")
+
+// ExactInstance is the complete observation accepted by the declare-lost
+// client. Account comes from Reservation.OwnerId, not from a request field or
+// the caller's default profile.
+type ExactInstance struct {
+ Account string `json:"account"`
+ Region string `json:"region"`
+ InstanceID string `json:"instance_id"`
+ State string `json:"state"`
+ RequestID string `json:"request_id"`
+}
+
+// ExactEC2API is the DescribeInstances slice of EC2API. The production EC2
+// client and every existing awsinv EC2 fake satisfy it; keeping the verifier
+// on this one-method view makes a write-capable client unnecessary.
+type ExactEC2API interface {
+ DescribeInstances(context.Context, *ec2.DescribeInstancesInput, ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error)
+}
+
+// NewEC2Reader resolves the operator's ordinary AWS credential chain and
+// returns the existing read-only EC2 seam in the enrolled region. It grants
+// no stop, terminate or restart operation.
+func NewEC2Reader(ctx context.Context, region string) (ExactEC2API, error) {
+ if strings.TrimSpace(region) == "" {
+ return nil, fmt.Errorf("%w: enrolled region is empty", ErrExactInstance)
+ }
+ cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(region))
+ if err != nil {
+ return nil, fmt.Errorf("%w: AWS config: %v", ErrExactInstance, err)
+ }
+ probeCtx, cancel := context.WithTimeout(ctx, credentialProbe)
+ defer cancel()
+ if _, err := cfg.Credentials.Retrieve(probeCtx); err != nil {
+ return nil, fmt.Errorf("%w: no usable AWS credential: %v", ErrExactInstance, err)
+ }
+ return factoryFor(cfg)(region).EC2, nil
+}
+
+// VerifyExactStoppedInstance makes one DescribeInstances call constrained to
+// the exact enrolled ID. Any API error, pagination, missing field, duplicate
+// result, owner mismatch or live state refuses; InvalidInstanceID.NotFound is
+// therefore never treated as termination evidence.
+func VerifyExactStoppedInstance(ctx context.Context, api ExactEC2API, account, region, instanceID string) (ExactInstance, error) {
+ wantAccount := strings.TrimSpace(account)
+ wantRegion := strings.TrimSpace(region)
+ wantID := strings.TrimSpace(instanceID)
+ if api == nil || wantAccount == "" || wantRegion == "" || wantID == "" {
+ return ExactInstance{}, fmt.Errorf("%w: incomplete enrolled cloud binding", ErrExactInstance)
+ }
+ out, err := api.DescribeInstances(ctx, &ec2.DescribeInstancesInput{InstanceIds: []string{wantID}}, func(o *ec2.Options) {
+ o.Region = wantRegion
+ })
+ if err != nil {
+ return ExactInstance{}, fmt.Errorf("%w: DescribeInstances %s in %s/%s: %v", ErrExactInstance, wantID, wantAccount, wantRegion, err)
+ }
+ if out == nil || aws.ToString(out.NextToken) != "" || len(out.Reservations) != 1 {
+ return ExactInstance{}, fmt.Errorf("%w: DescribeInstances returned a partial or ambiguous result", ErrExactInstance)
+ }
+ requestID, ok := awsmiddleware.GetRequestIDMetadata(out.ResultMetadata)
+ if !ok || strings.TrimSpace(requestID) == "" {
+ return ExactInstance{}, ErrExactInstance
+ }
+ r := out.Reservations[0]
+ owner := aws.ToString(r.OwnerId)
+ if owner == "" || owner != wantAccount || len(r.Instances) != 1 {
+ return ExactInstance{}, fmt.Errorf("%w: reservation owner or instance count does not match enrollment", ErrExactInstance)
+ }
+ in := r.Instances[0]
+ gotID := aws.ToString(in.InstanceId)
+ if gotID == "" || gotID != wantID || in.State == nil {
+ return ExactInstance{}, fmt.Errorf("%w: returned instance does not exactly match enrollment", ErrExactInstance)
+ }
+ state := string(in.State.Name)
+ if state != "stopped" && state != "terminated" {
+ return ExactInstance{}, fmt.Errorf("%w: instance %s is %q", ErrExactInstance, wantID, state)
+ }
+ return ExactInstance{Account: owner, Region: wantRegion, InstanceID: gotID, State: state, RequestID: requestID}, nil
+}
diff --git a/internal/awsinv/exact_instance_test.go b/internal/awsinv/exact_instance_test.go
new file mode 100644
index 000000000..872bb1ea0
--- /dev/null
+++ b/internal/awsinv/exact_instance_test.go
@@ -0,0 +1,103 @@
+package awsinv
+
+import (
+ "context"
+ "errors"
+ "testing"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
+ "github.com/aws/aws-sdk-go-v2/service/ec2"
+ "github.com/aws/aws-sdk-go-v2/service/ec2/types"
+ "github.com/aws/smithy-go/middleware"
+)
+
+type exactEC2 struct {
+ out *ec2.DescribeInstancesOutput
+ err error
+ in *ec2.DescribeInstancesInput
+ region string
+}
+
+func (f *exactEC2) DescribeInstances(_ context.Context, in *ec2.DescribeInstancesInput, options ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) {
+ f.in = in
+ var opts ec2.Options
+ for _, option := range options {
+ option(&opts)
+ }
+ f.region = opts.Region
+ return f.out, f.err
+}
+
+func exactOutput(owner, id string, state types.InstanceStateName) *ec2.DescribeInstancesOutput {
+ out := &ec2.DescribeInstancesOutput{Reservations: []types.Reservation{{
+ OwnerId: aws.String(owner),
+ Instances: []types.Instance{{InstanceId: aws.String(id), State: &types.InstanceState{Name: state}}},
+ }}}
+ awsmiddleware.SetRequestIDMetadata(&out.ResultMetadata, "aws-request-123")
+ return out
+}
+
+func TestVerifyExactStoppedInstanceAcceptsOnlyStoppedOrTerminated(t *testing.T) {
+ for _, state := range []types.InstanceStateName{types.InstanceStateNameStopped, types.InstanceStateNameTerminated} {
+ t.Run(string(state), func(t *testing.T) {
+ api := &exactEC2{out: exactOutput("123456789012", "i-123", state)}
+ got, err := VerifyExactStoppedInstance(t.Context(), api, "123456789012", "eu-west-1", "i-123")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got.Account != "123456789012" || got.Region != "eu-west-1" || got.InstanceID != "i-123" || got.State != string(state) || got.RequestID != "aws-request-123" {
+ t.Fatalf("observation = %+v", got)
+ }
+ if len(api.in.InstanceIds) != 1 || api.in.InstanceIds[0] != "i-123" {
+ t.Fatalf("DescribeInstances input = %+v", api.in)
+ }
+ if api.region != "eu-west-1" {
+ t.Fatalf("DescribeInstances region = %q", api.region)
+ }
+ })
+ }
+}
+
+func TestVerifyExactStoppedInstanceRefusesEveryIncompleteOrLiveAnswer(t *testing.T) {
+ notFound := errors.New("InvalidInstanceID.NotFound")
+ cases := map[string]*exactEC2{
+ "missing-request-id": {out: func() *ec2.DescribeInstancesOutput {
+ o := exactOutput("123456789012", "i-123", types.InstanceStateNameStopped)
+ o.ResultMetadata = middleware.Metadata{}
+ return o
+ }()},
+ "empty-request-id": {out: func() *ec2.DescribeInstancesOutput {
+ o := exactOutput("123456789012", "i-123", types.InstanceStateNameStopped)
+ awsmiddleware.SetRequestIDMetadata(&o.ResultMetadata, "")
+ return o
+ }()},
+ "blank-request-id": {out: func() *ec2.DescribeInstancesOutput {
+ o := exactOutput("123456789012", "i-123", types.InstanceStateNameStopped)
+ awsmiddleware.SetRequestIDMetadata(&o.ResultMetadata, " ")
+ return o
+ }()},
+ "not-found": {err: notFound},
+ "nil": {},
+ "no-reservation": {out: &ec2.DescribeInstancesOutput{}},
+ "partial-page": {out: func() *ec2.DescribeInstancesOutput {
+ o := exactOutput("123456789012", "i-123", types.InstanceStateNameStopped)
+ o.NextToken = aws.String("more")
+ return o
+ }()},
+ "wrong-owner": {out: exactOutput("999999999999", "i-123", types.InstanceStateNameStopped)},
+ "missing-owner": {out: exactOutput("", "i-123", types.InstanceStateNameStopped)},
+ "wrong-instance": {out: exactOutput("123456789012", "i-other", types.InstanceStateNameStopped)},
+ "running": {out: exactOutput("123456789012", "i-123", types.InstanceStateNameRunning)},
+ "missing-state": {out: &ec2.DescribeInstancesOutput{Reservations: []types.Reservation{{OwnerId: aws.String("123456789012"), Instances: []types.Instance{{InstanceId: aws.String("i-123")}}}}}},
+ "two-reservations": {out: &ec2.DescribeInstancesOutput{Reservations: []types.Reservation{{OwnerId: aws.String("123456789012")}, {OwnerId: aws.String("123456789012")}}}},
+ "two-instances": {out: &ec2.DescribeInstancesOutput{Reservations: []types.Reservation{{OwnerId: aws.String("123456789012"), Instances: []types.Instance{{InstanceId: aws.String("i-123")}, {InstanceId: aws.String("i-123")}}}}}},
+ }
+ for name, api := range cases {
+ t.Run(name, func(t *testing.T) {
+ if _, err := VerifyExactStoppedInstance(t.Context(), api, "123456789012", "eu-west-1", "i-123"); !errors.Is(err, ErrExactInstance) {
+ t.Fatalf("error = %v, want ErrExactInstance", err)
+ }
+ })
+ }
+}
diff --git a/internal/hostfence/challenge.go b/internal/hostfence/challenge.go
new file mode 100644
index 000000000..ced20cc67
--- /dev/null
+++ b/internal/hostfence/challenge.go
@@ -0,0 +1,237 @@
+// Package hostfence defines the operator-bound evidence used by a host loss
+// declaration. It does not stop or terminate compute and it does not mutate a
+// host claim.
+package hostfence
+
+import (
+ "context"
+ "crypto/ed25519"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/carlosframework/platform/internal/awsinv"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+const (
+ ChallengeTTL = 120 * time.Second
+ ObservationMaxAge = 60 * time.Second
+ ObservationMaxSkew = 5 * time.Second
+)
+
+var (
+ ErrChallenge = errors.New("hostfence: challenge refused")
+ ErrObservation = errors.New("hostfence: cloud observation refused")
+)
+
+type CloudBinding struct {
+ Provider string `json:"provider"`
+ Account string `json:"account"`
+ Region string `json:"region"`
+ InstanceID string `json:"instance_id"`
+}
+
+type Operation struct {
+ ID string `json:"id"`
+ Kind string `json:"kind"`
+ Phase string `json:"phase"`
+}
+
+type RecordObservation struct {
+ Key string `json:"key"`
+ ETag string `json:"etag"`
+ CommitState string `json:"commit_state"`
+}
+
+// Consequences is signed by the console so the confirmation cannot be
+// replayed for a different transition or a different set of claimed losses.
+type Consequences struct {
+ Transition string `json:"transition"`
+ DischargeLocal []hostclaim.ObligationRef `json:"discharge_local,omitempty"`
+ Chores []string `json:"chores,omitempty"`
+ PossibleWriteLoss bool `json:"possible_write_loss,omitempty"`
+ PossibleTimerLoss bool `json:"possible_timer_loss,omitempty"`
+ DestroyedStorageAssertion []string `json:"destroyed_storage_assertion,omitempty"`
+}
+
+// Challenge is the console's exact, short-lived snapshot of the transition
+// the operator inspected. Its signed envelope is submitted unchanged.
+type Challenge struct {
+ ID string `json:"id"`
+ Actor string `json:"actor"`
+ IssuedAt time.Time `json:"issued_at"`
+ ExpiresAt time.Time `json:"expires_at"`
+ Host string `json:"host"`
+ HolderIncarnation string `json:"holder_incarnation"`
+ HolderStamp uint64 `json:"holder_stamp"`
+ HolderSession uint64 `json:"holder_session"`
+ ClaimGen uint64 `json:"claim_gen"`
+ ClaimETag string `json:"claim_etag"`
+ Op Operation `json:"op"`
+ Record RecordObservation `json:"record"`
+ Box string `json:"box"`
+ Enrollment string `json:"enrollment"`
+ Cloud CloudBinding `json:"cloud"`
+ Consequences Consequences `json:"consequences"`
+}
+
+type CloudObservation struct {
+ ObservedAt time.Time `json:"observed_at"`
+ Account string `json:"account"`
+ Region string `json:"region"`
+ InstanceID string `json:"instance_id"`
+ State string `json:"state"`
+ RequestID string `json:"request_id"`
+}
+
+type OperatorAttestation struct {
+ MaintainComputeFence bool `json:"maintain_compute_fence"`
+ ColdQuarantineStartOnly bool `json:"cold_quarantine_start_only"`
+ Disks []string `json:"disks,omitempty"`
+}
+
+type Submission struct {
+ Challenge json.RawMessage `json:"challenge"`
+ Observation CloudObservation `json:"observation"`
+ RequestID string `json:"request_id"`
+ Attestation OperatorAttestation `json:"attestation"`
+ Confirmation string `json:"confirmation"`
+}
+
+// PrepareChallenge supplies the one permitted lifetime. The caller fills the
+// exact claim, record, actor, enrollment and consequence bindings first.
+func PrepareChallenge(now time.Time, c Challenge) (Challenge, error) {
+ c.IssuedAt = now.UTC()
+ c.ExpiresAt = c.IssuedAt.Add(ChallengeTTL)
+ if err := validateChallenge(c); err != nil {
+ return Challenge{}, err
+ }
+ return c, nil
+}
+
+func validateChallenge(c Challenge) error {
+ if c.ID == "" || c.Actor == "" || c.Host == "" || c.HolderIncarnation == "" ||
+ c.ClaimGen == 0 || c.ClaimETag == "" || c.Op.Kind == "" ||
+ c.Record.Key == "" || c.Record.CommitState == "" || c.Box == "" || c.Enrollment == "" ||
+ c.Cloud.Provider != "aws" || c.Cloud.Account == "" || c.Cloud.Region == "" || c.Cloud.InstanceID == "" ||
+ c.Consequences.Transition == "" || c.IssuedAt.IsZero() || c.ExpiresAt.IsZero() ||
+ !c.ExpiresAt.Equal(c.IssuedAt.Add(ChallengeTTL)) {
+ return fmt.Errorf("%w: incomplete or incorrectly timed challenge", ErrChallenge)
+ }
+ if c.Op.Kind == hostclaim.KindServing || c.Consequences.Transition == "destroyed-local-only" {
+ if c.Op.Kind != hostclaim.KindServing || c.Op.ID != "" || c.Op.Phase != "" ||
+ c.Consequences.Transition != "destroyed-local-only" || c.Consequences.PossibleWriteLoss ||
+ c.Consequences.PossibleTimerLoss || len(c.Consequences.Chores) != 0 ||
+ len(c.Consequences.DischargeLocal) == 0 ||
+ len(c.Consequences.DestroyedStorageAssertion) != len(c.Consequences.DischargeLocal) {
+ return ErrChallenge
+ }
+ want := map[string]bool{}
+ for _, ref := range c.Consequences.DischargeLocal {
+ key := ref.Op + "/" + ref.ID + "/" + ref.Retirement
+ if ref.ID == "" || ref.Op == "" || ref.Retirement == "" || want[key] {
+ return ErrChallenge
+ }
+ want[key] = true
+ }
+ for _, assertion := range c.Consequences.DestroyedStorageAssertion {
+ if !want[assertion] {
+ return ErrChallenge
+ }
+ delete(want, assertion)
+ }
+ if len(want) != 0 {
+ return ErrChallenge
+ }
+ } else if c.Op.ID == "" || len(c.Consequences.DischargeLocal) != 0 {
+ return ErrChallenge
+ }
+ if (c.Record.CommitState == "absent") != (c.Record.ETag == "") {
+ return fmt.Errorf("%w: record absence and ETag are inconsistent", ErrChallenge)
+ }
+ return nil
+}
+
+// SignChallenge uses the existing root release signer and its existing
+// payload envelope. A scoped release signer is rejected by that primitive.
+func SignChallenge(sg release.Signer, c Challenge) ([]byte, error) {
+ if err := validateChallenge(c); err != nil {
+ return nil, err
+ }
+ wire, err := release.SignRootEnvelope(sg, c)
+ if err != nil {
+ return nil, fmt.Errorf("%w: %v", ErrChallenge, err)
+ }
+ return wire, nil
+}
+
+// VerifyChallenge authenticates and decodes the envelope before checking its
+// lifetime. A challenge at its expiry instant is expired.
+func VerifyChallenge(pub ed25519.PublicKey, wire []byte, now time.Time) (Challenge, error) {
+ var c Challenge
+ if err := release.VerifyRootEnvelope(pub, wire, &c); err != nil {
+ return Challenge{}, fmt.Errorf("%w: %v", ErrChallenge, err)
+ }
+ if err := validateChallenge(c); err != nil {
+ return Challenge{}, err
+ }
+ now = now.UTC()
+ if c.IssuedAt.After(now.Add(ObservationMaxSkew)) || !now.Before(c.ExpiresAt) {
+ return Challenge{}, fmt.Errorf("%w: expired or future challenge", ErrChallenge)
+ }
+ return c, nil
+}
+
+// ObserveForChallenge is the CLI-side verifier. It asks AWS for the exact
+// enrolled instance after inspection and returns only an accepted stopped or
+// terminated observation. It never makes a cloud write call.
+func ObserveForChallenge(ctx context.Context, api awsinv.ExactEC2API, c Challenge, now func() time.Time) (CloudObservation, error) {
+ if err := validateChallenge(c); err != nil {
+ return CloudObservation{}, err
+ }
+ if now == nil {
+ return CloudObservation{}, fmt.Errorf("%w: no observation clock", ErrObservation)
+ }
+ started := now().UTC()
+ if !started.After(c.IssuedAt) {
+ return CloudObservation{}, fmt.Errorf("%w: observation did not begin after challenge issuance", ErrObservation)
+ }
+ got, err := awsinv.VerifyExactStoppedInstance(ctx, api, c.Cloud.Account, c.Cloud.Region, c.Cloud.InstanceID)
+ if err != nil {
+ return CloudObservation{}, fmt.Errorf("%w: %v", ErrObservation, err)
+ }
+ consumedAt := now().UTC()
+ // observed_at is the conservative request start, not the response time:
+ // a DescribeInstances call blocked for a minute cannot make an old read
+ // appear fresh merely by returning now.
+ o := CloudObservation{ObservedAt: started, Account: got.Account, Region: got.Region, InstanceID: got.InstanceID, State: got.State, RequestID: got.RequestID}
+ if err := ValidateObservation(c, o, consumedAt); err != nil {
+ return CloudObservation{}, err
+ }
+ return o, nil
+}
+
+// ValidateObservation is the console's binding and freshness check. It does
+// not claim that AWS signed the observation: it validates the authenticated
+// operator assertion carried by the submission.
+func ValidateObservation(c Challenge, o CloudObservation, now time.Time) error {
+ now = now.UTC()
+ if err := validateChallenge(c); err != nil {
+ return err
+ }
+ if o.Account != c.Cloud.Account || o.Region != c.Cloud.Region || o.InstanceID != c.Cloud.InstanceID ||
+ (o.State != "stopped" && o.State != "terminated") || strings.TrimSpace(o.RequestID) == "" {
+ return fmt.Errorf("%w: cloud identity or state does not match the challenge", ErrObservation)
+ }
+ if !o.ObservedAt.After(c.IssuedAt) || o.ObservedAt.After(now.Add(ObservationMaxSkew)) || now.Sub(o.ObservedAt) > ObservationMaxAge {
+ return fmt.Errorf("%w: observation is stale, predates inspection, or is too far in the future", ErrObservation)
+ }
+ if !now.Before(c.ExpiresAt) {
+ return fmt.Errorf("%w: challenge expired", ErrObservation)
+ }
+ return nil
+}
diff --git a/internal/hostfence/challenge_test.go b/internal/hostfence/challenge_test.go
new file mode 100644
index 000000000..32a739503
--- /dev/null
+++ b/internal/hostfence/challenge_test.go
@@ -0,0 +1,263 @@
+package hostfence
+
+import (
+ "context"
+ "crypto/ed25519"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "reflect"
+ "testing"
+ "time"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
+ "github.com/aws/aws-sdk-go-v2/service/ec2"
+ "github.com/aws/aws-sdk-go-v2/service/ec2/types"
+ "github.com/aws/smithy-go/middleware"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+type ec2Answer struct {
+ out *ec2.DescribeInstancesOutput
+ err error
+}
+
+func (f ec2Answer) DescribeInstances(_ context.Context, _ *ec2.DescribeInstancesInput, _ ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) {
+ return f.out, f.err
+}
+
+func challengeFixture(t *testing.T, now time.Time) Challenge {
+ t.Helper()
+ c, err := PrepareChallenge(now, Challenge{
+ ID: "loss-1", Actor: "operator@example.com", Host: "app.example.com",
+ HolderIncarnation: "inc-1", HolderStamp: 7, HolderSession: 8,
+ ClaimGen: 9, ClaimETag: "etag-claim",
+ Op: Operation{ID: "op-1", Kind: "moving", Phase: "copying"},
+ Record: RecordObservation{Key: "apps/bab/app/instances/app.example.com.json", ETag: "etag-record", CommitState: "committed"},
+ Box: "box-1", Enrollment: "0123456789abcdef0123456789abcdef",
+ Cloud: CloudBinding{Provider: "aws", Account: "123456789012", Region: "eu-west-1", InstanceID: "i-123"},
+ Consequences: Consequences{Transition: "declare-lost", Chores: []string{"park-local/1"}, PossibleWriteLoss: true},
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ return c
+}
+
+func TestChallengeRootEnvelopeAndLifetime(t *testing.T) {
+ pub, priv, err := ed25519.GenerateKey(nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ now := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC)
+ c := challengeFixture(t, now)
+ wire, err := SignChallenge(release.RootSigner(priv), c)
+ if err != nil {
+ t.Fatal(err)
+ }
+ got, err := VerifyChallenge(pub, wire, now.Add(119*time.Second))
+ if err != nil || got.ID != c.ID || got.Cloud != c.Cloud {
+ t.Fatalf("verify = %+v, %v", got, err)
+ }
+ if _, err := VerifyChallenge(pub, wire, c.ExpiresAt); !errors.Is(err, ErrChallenge) {
+ t.Fatalf("expiry error = %v", err)
+ }
+ wire[len(wire)/2] ^= 1
+ if _, err := VerifyChallenge(pub, wire, now); !errors.Is(err, ErrChallenge) {
+ t.Fatalf("tamper error = %v", err)
+ }
+}
+
+func TestChallengeModelsPresentAndAbsentRecordObservations(t *testing.T) {
+ now := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC)
+ _, priv, err := ed25519.GenerateKey(nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ c := challengeFixture(t, now)
+ c.HolderStamp, c.HolderSession = 0, 0
+ if _, err := SignChallenge(release.RootSigner(priv), c); err != nil {
+ t.Fatalf("explicit zero holder binding refused: %v", err)
+ }
+ c.Record = RecordObservation{Key: c.Record.Key, CommitState: "absent"}
+ if _, err := SignChallenge(release.RootSigner(priv), c); err != nil {
+ t.Fatalf("explicit absent record refused: %v", err)
+ }
+ for name, breakIt := range map[string]func(*Challenge){
+ "present-without-etag": func(c *Challenge) { c.Record.ETag = "" },
+ "absent-with-etag": func(c *Challenge) { c.Record.CommitState = "absent" },
+ } {
+ t.Run(name, func(t *testing.T) {
+ c := challengeFixture(t, now)
+ breakIt(&c)
+ if _, err := SignChallenge(release.RootSigner(priv), c); !errors.Is(err, ErrChallenge) {
+ t.Fatalf("error = %v", err)
+ }
+ })
+ }
+}
+
+func TestObserveForChallengeBindsExactStoppedInstance(t *testing.T) {
+ issued := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC)
+ c := challengeFixture(t, issued)
+ api := ec2Answer{out: &ec2.DescribeInstancesOutput{Reservations: []types.Reservation{{
+ OwnerId: aws.String(c.Cloud.Account), Instances: []types.Instance{{InstanceId: aws.String(c.Cloud.InstanceID), State: &types.InstanceState{Name: types.InstanceStateNameStopped}}},
+ }}}}
+ awsmiddleware.SetRequestIDMetadata(&api.out.ResultMetadata, "aws-request-123")
+ times := []time.Time{issued.Add(time.Second), issued.Add(2 * time.Second)}
+ i := 0
+ o, err := ObserveForChallenge(t.Context(), api, c, func() time.Time { v := times[i]; i++; return v })
+ if err != nil {
+ t.Fatal(err)
+ }
+ if o.State != "stopped" || o.ObservedAt != times[0] || o.RequestID != "aws-request-123" {
+ t.Fatalf("observation = %+v", o)
+ }
+
+}
+
+func TestObserveForChallengeRefusesMissingRequestID(t *testing.T) {
+ issued := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC)
+ c := challengeFixture(t, issued)
+ for _, id := range []string{"missing", "", " "} {
+ t.Run(id, func(t *testing.T) {
+ api := ec2Answer{out: &ec2.DescribeInstancesOutput{Reservations: []types.Reservation{{
+ OwnerId: aws.String(c.Cloud.Account), Instances: []types.Instance{{InstanceId: aws.String(c.Cloud.InstanceID), State: &types.InstanceState{Name: types.InstanceStateNameStopped}}},
+ }}}}
+ api.out.ResultMetadata = middleware.Metadata{}
+ if id != "missing" {
+ awsmiddleware.SetRequestIDMetadata(&api.out.ResultMetadata, id)
+ }
+ if _, err := ObserveForChallenge(t.Context(), api, c, func() time.Time { return issued.Add(time.Second) }); !errors.Is(err, ErrObservation) {
+ t.Fatalf("missing request ID accepted: %v", err)
+ }
+ })
+ }
+}
+
+func TestObserveForChallengeRefusesAReadThatBlockedPastFreshness(t *testing.T) {
+ issued := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC)
+ c := challengeFixture(t, issued)
+ api := ec2Answer{out: &ec2.DescribeInstancesOutput{Reservations: []types.Reservation{{
+ OwnerId: aws.String(c.Cloud.Account), Instances: []types.Instance{{InstanceId: aws.String(c.Cloud.InstanceID), State: &types.InstanceState{Name: types.InstanceStateNameStopped}}},
+ }}}}
+ awsmiddleware.SetRequestIDMetadata(&api.out.ResultMetadata, "aws-request-123")
+ times := []time.Time{issued.Add(time.Second), issued.Add(62 * time.Second)}
+ i := 0
+ if _, err := ObserveForChallenge(t.Context(), api, c, func() time.Time { v := times[i]; i++; return v }); !errors.Is(err, ErrObservation) {
+ t.Fatalf("error = %v", err)
+ }
+}
+
+func TestValidateObservationFreshnessAndBinding(t *testing.T) {
+ issued := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC)
+ c := challengeFixture(t, issued)
+ valid := CloudObservation{ObservedAt: issued.Add(10 * time.Second), Account: c.Cloud.Account, Region: c.Cloud.Region, InstanceID: c.Cloud.InstanceID, State: "terminated", RequestID: "aws-request-123"}
+ if err := ValidateObservation(c, valid, issued.Add(60*time.Second)); err != nil {
+ t.Fatal(err)
+ }
+ checks := []CloudObservation{
+ func() CloudObservation { o := valid; o.RequestID = ""; return o }(),
+ func() CloudObservation { o := valid; o.RequestID = " "; return o }(),
+ func() CloudObservation { o := valid; o.ObservedAt = issued; return o }(),
+ func() CloudObservation { o := valid; o.ObservedAt = issued.Add(11 * time.Second); return o }(),
+ func() CloudObservation { o := valid; o.Account = "999999999999"; return o }(),
+ func() CloudObservation { o := valid; o.State = "running"; return o }(),
+ }
+ for i, o := range checks {
+ now := issued.Add(70 * time.Second)
+ if i < 2 {
+ now = issued.Add(60 * time.Second)
+ }
+ if i == 3 {
+ now = issued.Add(5 * time.Second)
+ }
+ if err := ValidateObservation(c, o, now); !errors.Is(err, ErrObservation) {
+ t.Errorf("case %d error = %v", i, err)
+ }
+ }
+}
+
+func TestServingDestroyedLocalOnlyChallengeBindsOriginalChore(t *testing.T) {
+ now := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC)
+ pub, priv, err := ed25519.GenerateKey(nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ c := challengeFixture(t, now)
+ c.Op = Operation{Kind: hostclaim.KindServing}
+ c.Consequences = Consequences{
+ Transition: "destroyed-local-only",
+ DischargeLocal: []hostclaim.ObligationRef{{ID: "local", Op: "discovery", Retirement: "retired-installation"}},
+ DestroyedStorageAssertion: []string{"discovery/local/retired-installation"},
+ }
+ wire, err := SignChallenge(release.RootSigner(priv), c)
+ if err != nil {
+ t.Fatal(err)
+ }
+ got, err := VerifyChallenge(pub, wire, now.Add(time.Second))
+ if err != nil || !reflect.DeepEqual(got, c) {
+ t.Fatalf("serving binding round trip = %+v %v", got, err)
+ }
+ for name, mutate := range map[string]func(*Challenge){
+ "generic": func(c *Challenge) { c.Consequences.Transition = "declare-lost" },
+ "non-serving": func(c *Challenge) { c.Op.Kind = "moving" },
+ "current-op": func(c *Challenge) { c.Op.ID = "discovery" },
+ "phase": func(c *Challenge) { c.Op.Phase = "recover" },
+ "claim-gen": func(c *Challenge) { c.ClaimGen = 0 },
+ "claim-etag": func(c *Challenge) { c.ClaimETag = "" },
+ "write-loss": func(c *Challenge) { c.Consequences.PossibleWriteLoss = true },
+ "timer-loss": func(c *Challenge) { c.Consequences.PossibleTimerLoss = true },
+ "unbound-chore": func(c *Challenge) { c.Consequences.Chores = []string{"extra"} },
+ "empty": func(c *Challenge) { c.Consequences.DischargeLocal = nil },
+ "original-op": func(c *Challenge) { c.Consequences.DischargeLocal[0].Op = "" },
+ "retirement": func(c *Challenge) { c.Consequences.DischargeLocal[0].Retirement = "" },
+ "proof": func(c *Challenge) {
+ c.Consequences.DestroyedStorageAssertion = []string{"other/local/retired-installation"}
+ },
+ } {
+ t.Run(name, func(t *testing.T) {
+ bad := c
+ bad.Consequences.DischargeLocal = append([]hostclaim.ObligationRef(nil), c.Consequences.DischargeLocal...)
+ mutate(&bad)
+ if _, err := SignChallenge(release.RootSigner(priv), bad); !errors.Is(err, ErrChallenge) {
+ t.Fatalf("widened challenge = %v", err)
+ }
+ })
+ }
+ // The signature covers the exact revision and each original chore identity.
+ for _, field := range []string{"claim_gen", "claim_etag", "op", "retirement"} {
+ var envelope struct {
+ Payload string `json:"payload"`
+ Sig string `json:"sig"`
+ }
+ if err := json.Unmarshal(wire, &envelope); err != nil {
+ t.Fatal(err)
+ }
+ var payload map[string]any
+ raw, err := base64.StdEncoding.DecodeString(envelope.Payload)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := json.Unmarshal(raw, &payload); err != nil {
+ t.Fatal(err)
+ }
+ switch field {
+ case "claim_gen":
+ payload[field] = float64(c.ClaimGen + 1)
+ case "claim_etag":
+ payload[field] = "other-etag"
+ default:
+ refs := payload["consequences"].(map[string]any)["discharge_local"].([]any)
+ refs[0].(map[string]any)[field] = "other-identity"
+ }
+ raw, _ = json.Marshal(payload)
+ envelope.Payload = base64.StdEncoding.EncodeToString(raw)
+ tampered, _ := json.Marshal(envelope)
+ if _, err := VerifyChallenge(pub, tampered, now); !errors.Is(err, ErrChallenge) {
+ t.Fatalf("changed %s retained signature: %v", field, err)
+ }
+ }
+}
diff --git a/internal/hostquarantine/quarantine.go b/internal/hostquarantine/quarantine.go
new file mode 100644
index 000000000..529692779
--- /dev/null
+++ b/internal/hostquarantine/quarantine.go
@@ -0,0 +1,116 @@
+// Package hostquarantine is the deployment-wide admission gate for enrolled
+// boxes. The authoritative directory implementation is deliberately outside
+// this package; callers supply an exact-object reader and every Check performs
+// a fresh lookup.
+package hostquarantine
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+)
+
+const (
+ StateActive = "active"
+ StateQuarantined = "quarantined"
+ StateRetired = "retired"
+)
+
+var (
+ ErrAdmission = errors.New("hostquarantine: admission refused")
+ ErrRecord = errors.New("hostquarantine: malformed retirement completion")
+)
+
+type Actor struct {
+ Enrollment string `json:"enrollment"`
+ State string `json:"state"`
+ QuarantineRevision uint64 `json:"quarantine_revision"`
+}
+
+// ActorDirectory reads one exact actor from the root-signed deployment
+// directory. Implementations must return an error for missing, unreadable or
+// unverifiable authority.
+type ActorDirectory interface {
+ Lookup(context.Context, string) (Actor, error)
+}
+
+// Check admits only the exact active enrollment returned by a fresh lookup.
+// Missing actors, unreadable authority, identity mismatches, quarantine,
+// retirement and unknown states all fail closed.
+func Check(ctx context.Context, dir ActorDirectory, box, enrollment string) error {
+ if dir == nil || box == "" || enrollment == "" {
+ return fmt.Errorf("%w: actor identity or directory is absent", ErrAdmission)
+ }
+ a, err := dir.Lookup(ctx, box)
+ if err != nil {
+ return fmt.Errorf("%w: actor directory lookup: %v", ErrAdmission, err)
+ }
+ if a.Enrollment == "" || a.Enrollment != enrollment {
+ return fmt.Errorf("%w: enrollment mismatch", ErrAdmission)
+ }
+ if a.State != StateActive {
+ return fmt.Errorf("%w: actor state %q", ErrAdmission, a.State)
+ }
+ return nil
+}
+
+type RetirementEntry struct {
+ Host string `json:"host"`
+ Incarnation string `json:"incarnation"`
+ Stamp uint64 `json:"stamp"`
+ RetirementID string `json:"retirement_id"`
+ JournalSHA256 string `json:"journal_sha256"`
+}
+
+// RetirementCompletion is the returning machine's durable retirement
+// receipt. Readmission also requires the matching fresh private box report;
+// this object alone does not grant admission.
+type RetirementCompletion struct {
+ V int `json:"v"`
+ Box string `json:"box"`
+ Enrollment string `json:"enrollment"`
+ InstanceID string `json:"instance_id"`
+ QuarantineRevision uint64 `json:"quarantine_revision"`
+ BootID string `json:"boot_id"`
+ CompletedAt time.Time `json:"completed_at"`
+ Complete bool `json:"complete"`
+ InventorySHA256 string `json:"inventory_sha256"`
+ Entries []RetirementEntry `json:"entries"`
+}
+
+var sha256RE = regexp.MustCompile(`^[0-9a-f]{64}$`)
+
+func safeSegment(s string) bool {
+ return s != "" && s != "." && s != ".." && !strings.ContainsAny(s, "/\\")
+}
+
+func RetirementKey(box, enrollment string, revision uint64) (string, error) {
+ if !safeSegment(box) || !safeSegment(enrollment) || revision == 0 {
+ return "", fmt.Errorf("%w: invalid key identity", ErrRecord)
+ }
+ return "control/host-retirement/" + box + "/" + enrollment + "/" + strconv.FormatUint(revision, 10) + ".json", nil
+}
+
+func (r RetirementCompletion) Validate() error {
+ if r.V != 1 || !safeSegment(r.Box) || !safeSegment(r.Enrollment) || r.InstanceID == "" ||
+ r.QuarantineRevision == 0 || r.BootID == "" {
+ return fmt.Errorf("%w: invalid header", ErrRecord)
+ }
+ if r.Complete {
+ if r.CompletedAt.IsZero() || !sha256RE.MatchString(r.InventorySHA256) || r.Entries == nil {
+ return fmt.Errorf("%w: completed receipt lacks durable inventory evidence", ErrRecord)
+ }
+ } else if !r.CompletedAt.IsZero() {
+ return fmt.Errorf("%w: incomplete receipt has completed_at", ErrRecord)
+ }
+ for i, e := range r.Entries {
+ if e.Host == "" || e.Incarnation == "" || e.Stamp == 0 || e.RetirementID == "" || !sha256RE.MatchString(e.JournalSHA256) {
+ return fmt.Errorf("%w: invalid entry %d", ErrRecord, i)
+ }
+ }
+ return nil
+}
diff --git a/internal/hostquarantine/quarantine_test.go b/internal/hostquarantine/quarantine_test.go
new file mode 100644
index 000000000..9f0890fd3
--- /dev/null
+++ b/internal/hostquarantine/quarantine_test.go
@@ -0,0 +1,77 @@
+package hostquarantine
+
+import (
+ "context"
+ "errors"
+ "os"
+ "testing"
+ "time"
+)
+
+type fakeDirectory struct {
+ actor Actor
+ err error
+ calls int
+}
+
+func (f *fakeDirectory) Lookup(_ context.Context, _ string) (Actor, error) {
+ f.calls++
+ return f.actor, f.err
+}
+
+func TestCheckRequiresFreshExactActiveEnrollment(t *testing.T) {
+ d := &fakeDirectory{actor: Actor{Enrollment: "enroll-1", State: StateActive, QuarantineRevision: 3}}
+ if err := Check(t.Context(), d, "box-1", "enroll-1"); err != nil {
+ t.Fatal(err)
+ }
+ d.actor.State = StateQuarantined
+ if err := Check(t.Context(), d, "box-1", "enroll-1"); !errors.Is(err, ErrAdmission) {
+ t.Fatalf("quarantine error = %v", err)
+ }
+ if d.calls != 2 {
+ t.Fatalf("directory calls = %d, want a fresh lookup per check", d.calls)
+ }
+}
+
+func TestCheckFailsClosed(t *testing.T) {
+ cases := map[string]*fakeDirectory{
+ "missing": {err: os.ErrNotExist},
+ "unreadable": {err: errors.New("signature refused")},
+ "mismatch": {actor: Actor{Enrollment: "other", State: StateActive}},
+ "retired": {actor: Actor{Enrollment: "enroll-1", State: StateRetired}},
+ "unknown": {actor: Actor{Enrollment: "enroll-1", State: "paused"}},
+ }
+ for name, d := range cases {
+ t.Run(name, func(t *testing.T) {
+ if err := Check(t.Context(), d, "box-1", "enroll-1"); !errors.Is(err, ErrAdmission) {
+ t.Fatalf("error = %v", err)
+ }
+ })
+ }
+}
+
+func TestRetirementCompletionKeyAndValidation(t *testing.T) {
+ key, err := RetirementKey("box-1", "enroll-1", 7)
+ if err != nil || key != "control/host-retirement/box-1/enroll-1/7.json" {
+ t.Fatalf("key = %q, %v", key, err)
+ }
+ hash := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
+ r := RetirementCompletion{
+ V: 1, Box: "box-1", Enrollment: "enroll-1", InstanceID: "i-123", QuarantineRevision: 7,
+ BootID: "boot-1", CompletedAt: time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC), Complete: true,
+ InventorySHA256: hash,
+ Entries: []RetirementEntry{{Host: "app.example.com", Incarnation: "inc-1", Stamp: 8, RetirementID: "retire-1", JournalSHA256: hash}},
+ }
+ if err := r.Validate(); err != nil {
+ t.Fatal(err)
+ }
+ r.Entries = nil
+ if err := r.Validate(); !errors.Is(err, ErrRecord) {
+ t.Fatalf("nil entries error = %v", err)
+ }
+ r.Entries = []RetirementEntry{{Host: "app.example.com", Incarnation: "inc-1", Stamp: 8, RetirementID: "retire-1", JournalSHA256: hash}}
+ r.Entries[0].JournalSHA256 = ""
+ if err := r.Validate(); !errors.Is(err, ErrRecord) {
+ t.Fatalf("error = %v", err)
+ }
+}