Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
452 changes: 452 additions & 0 deletions internal/control/envelope.go

Large diffs are not rendered by default.

146 changes: 146 additions & 0 deletions internal/control/envelope_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Copyright 2026 CtrlBoard.dev
// SPDX-License-Identifier: Apache-2.0

package control

import (
"bytes"
"encoding/json"
"errors"
"strings"
"testing"
"time"

"github.com/thelostorbital/ctrldb/internal/domain"
)

func TestEnvelopeSealParseRoundTripIsCanonical(t *testing.T) {
t.Parallel()
envelope := fixtureEnvelope(t)
encoded, err := envelope.CanonicalJSON()
if err != nil {
t.Fatalf("CanonicalJSON() error: %v", err)
}
parsed, err := ParseBootstrapEnvelope(encoded)
if err != nil {
t.Fatalf("ParseBootstrapEnvelope() error: %v", err)
}
reencoded, err := parsed.CanonicalJSON()
if err != nil || !bytes.Equal(reencoded, encoded) || parsed.SHA256() != envelope.SHA256() {
t.Fatalf("round trip changed the envelope (err=%v)", err)
}
if parsed.OperationID() != fixtureOperationID || parsed.AuditBucket() == parsed.ControlBucket() ||
parsed.AuditBucketLocation() != fixtureRegion || parsed.Plan().DocumentHash() != envelope.Plan().DocumentHash() ||
parsed.FirstJournalEntry().Sequence != 1 || !strings.HasPrefix(parsed.FirstJournalObjectName().String(), "operations/disposable-test/"+fixtureOperationID+"/steps/00000000000000000001-state-discover.json") {
t.Fatalf("parsed accessors = %#v", parsed.payload)
}
observations := parsed.ExpectedObservations()
if len(observations) != len(parsed.Plan().DesiredResources()) {
t.Fatalf("expected observations = %d", len(observations))
}
for _, observation := range observations {
if observation.Expectation != ExpectationAbsent || observation.ProviderID == "" {
t.Fatalf("observation = %#v", observation)
}
}
// Second seal of the identical seed reproduces the identical hash.
again, err := SealBootstrapEnvelope(fixtureSeed(t))
if err != nil || again.SHA256() != envelope.SHA256() {
t.Fatalf("seal is not deterministic (err=%v)", err)
}
// No credential-shaped field names or provider output are present.
lower := strings.ToLower(string(encoded))
for _, forbidden := range []string{"token", "password", "secret", "authorization", "selflink", "argv", "command\"", "gcloud"} {
if strings.Contains(lower, forbidden) {
t.Fatalf("envelope contains forbidden material %q", forbidden)
}
}
}

func TestEnvelopeParseRejectsTamperingAndAmbiguity(t *testing.T) {
t.Parallel()
envelope := fixtureEnvelope(t)
encoded, _ := envelope.CanonicalJSON()

flipped := append([]byte(nil), encoded...)
index := bytes.Index(flipped, []byte(`"auditBucket":"`)) + len(`"auditBucket":"`)
flipped[index] ^= 0x01
hashIndex := bytes.LastIndex(encoded, []byte(`"envelopeSha256":"`)) + len(`"envelopeSha256":"`)
hashFlipped := append([]byte(nil), encoded...)
if hashFlipped[hashIndex] == '0' {
hashFlipped[hashIndex] = '1'
} else {
hashFlipped[hashIndex] = '0'
}
duplicate := bytes.Replace(encoded, []byte(`"envelope":{`), []byte(`"envelope":{"workflowId":"WF-TEST-01",`), 1)
unknown := bytes.Replace(encoded, []byte(`"schemaVersion":`), []byte(`"extra":1,"schemaVersion":`), 1)
spaced := bytes.Replace(encoded, []byte(`"schemaVersion":`), []byte(`"schemaVersion": `), 1)
tests := map[string][]byte{
"empty": nil, "null": []byte("null"), "trailing": append(append([]byte(nil), encoded...), '\n', '{', '}'),
"payload byte flipped": flipped, "hash flipped": hashFlipped, "duplicate key": duplicate,
"unknown field": unknown, "noncanonical whitespace": spaced, "oversized": bytes.Repeat([]byte("["), MaxEnvelopeBytes+1),
}
for name, input := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()
if _, err := ParseBootstrapEnvelope(input); !errors.Is(err, ErrInvalidEnvelope) {
t.Fatalf("ParseBootstrapEnvelope() error = %v, want ErrInvalidEnvelope", err)
}
})
}
}

func TestEnvelopeSealRejectsCrossBindingFailures(t *testing.T) {
t.Parallel()
base := fixtureSeed(t)
tests := []struct {
name string
mutate func(seed *EnvelopeSeed)
want error
}{
{"operation id", func(seed *EnvelopeSeed) { seed.OperationID = "op-XYZ" }, ErrInvalidEnvelope},
{"journal operation mismatch", func(seed *EnvelopeSeed) { seed.FirstJournalEntry.OperationID = "op-fedcba9876543210" }, ErrInvalidEnvelope},
{"journal contract mismatch", func(seed *EnvelopeSeed) { seed.FirstJournalEntry.ContractHash = repeatHex("b") }, ErrInvalidEnvelope},
{"journal not first", func(seed *EnvelopeSeed) { seed.FirstJournalEntry.Sequence = 2 }, ErrInvalidEnvelope},
{"journal not discover", func(seed *EnvelopeSeed) { seed.FirstJournalEntry.OperationState = domain.OperationValidate }, ErrInvalidEnvelope},
{"journal after seal", func(seed *EnvelopeSeed) { seed.FirstJournalEntry.RecordedAt = seed.SealedAt.Add(time.Second) }, ErrInvalidEnvelope},
{"seal before plan", func(seed *EnvelopeSeed) { seed.SealedAt = fixtureNow.Add(-time.Second) }, ErrInvalidEnvelope},
{"seal after approval expiry", func(seed *EnvelopeSeed) { seed.SealedAt = seed.Approval.ValidUntil }, ErrInvalidEnvelope},
{"seal not UTC", func(seed *EnvelopeSeed) { seed.SealedAt = seed.SealedAt.In(time.FixedZone("X", 3600)) }, ErrInvalidEnvelope},
{"approval plan hash", func(seed *EnvelopeSeed) { seed.Approval.PlanV1Hash = repeatHex("c") }, ErrInvalidApprovalProof},
{"approval document hash", func(seed *EnvelopeSeed) { seed.Approval.PlanDocumentSHA256 = repeatHex("c") }, ErrInvalidApprovalProof},
{"approval account", func(seed *EnvelopeSeed) { seed.Approval.ApprovedBy = "other@example.invalid" }, ErrInvalidApprovalProof},
{"approval class", func(seed *EnvelopeSeed) { seed.Approval.ApprovalClass = domain.ApprovalRead }, ErrInvalidApprovalProof},
{"approval proof hash", func(seed *EnvelopeSeed) { seed.Approval.ProofSHA256 = repeatHex("d") }, ErrInvalidApprovalProof},
{"approval after plan expiry", func(seed *EnvelopeSeed) { seed.Approval.ValidUntil = fixtureNow.Add(2 * time.Hour) }, ErrInvalidApprovalProof},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
seed := base
test.mutate(&seed)
if _, err := SealBootstrapEnvelope(seed); !errors.Is(err, test.want) {
t.Fatalf("SealBootstrapEnvelope() error = %v, want %v", err, test.want)
}
})
}
}

func TestNewApprovalProofRejectsWindowsOutsidePlan(t *testing.T) {
t.Parallel()
plan := fixturePlan(t)
if _, err := NewApprovalProof(plan, fixtureAccount, fixtureNow.Add(-time.Second), fixtureNow.Add(time.Minute)); !errors.Is(err, ErrInvalidApprovalProof) {
t.Fatalf("approval before plan creation accepted: %v", err)
}
if _, err := NewApprovalProof(plan, "someone-else@example.invalid", fixtureNow, fixtureNow.Add(time.Minute)); !errors.Is(err, ErrInvalidApprovalProof) {
t.Fatalf("approval by a non-principal accepted: %v", err)
}
proof, err := NewApprovalProof(plan, fixtureAccount, fixtureNow, fixtureNow.Add(time.Hour))
if err != nil {
t.Fatalf("NewApprovalProof() error: %v", err)
}
encoded, _ := json.Marshal(proof)
if !strings.Contains(string(encoded), `"approvalClass":"security-sensitive"`) && !strings.Contains(string(encoded), `"approvalClass":"`) {
t.Fatalf("approval encoding = %s", encoded)
}
}
218 changes: 218 additions & 0 deletions internal/control/fixture_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
// Copyright 2026 CtrlBoard.dev
// SPDX-License-Identifier: Apache-2.0

package control

import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"os"
"testing"
"time"

"github.com/thelostorbital/ctrldb/internal/config"
"github.com/thelostorbital/ctrldb/internal/domain"
"github.com/thelostorbital/ctrldb/internal/isolation/bootstrap"
"github.com/thelostorbital/ctrldb/internal/observation"
)

const (
fixtureAccount = "operator@example.invalid"
fixtureProject = "example-project"
fixtureRegion = "us-central1"
fixtureZone = "us-central1-a"
fixtureOperationID = "op-0123456789abcdef"
fixturePlanID = "plan-0123456789abcdef"
)

var fixtureNow = time.Date(2026, 9, 9, 12, 1, 0, 0, time.UTC)

// fixturePermissions mirrors the M1-04 step registry exactly; a provider
// permission prover would emit this list.
var fixturePermissions = []struct {
step string
permissions []string
}{
{"k1-audit-bootstrap", []string{"storage.buckets.create", "storage.buckets.get", "storage.objects.create", "storage.objects.get"}},
{"k1-retention-lock", []string{"storage.buckets.get", "storage.buckets.update"}},
{"k2-control-bucket", []string{"storage.buckets.create", "storage.buckets.get", "storage.buckets.update"}},
{"k3-bucket-iam", []string{"storage.buckets.getIamPolicy", "storage.buckets.setIamPolicy"}},
{"k4-seed-control", []string{"storage.objects.create", "storage.objects.get"}},
{"k5-lock-round-trip", []string{"storage.objects.create", "storage.objects.get", "storage.objects.update"}},
{"t1-network", []string{"compute.networks.create", "compute.networks.get"}},
{"t2-subnet", []string{"compute.subnetworks.create", "compute.subnetworks.get"}},
{"t3-nat", []string{"compute.routers.create", "compute.routers.get", "compute.routers.update"}},
{"t4-firewall", []string{"compute.firewalls.create", "compute.firewalls.get"}},
{"t5-identities", []string{"iam.roles.create", "iam.roles.get", "iam.roles.update", "iam.serviceAccounts.create", "iam.serviceAccounts.get", "iam.serviceAccounts.setIamPolicy", "resourcemanager.projects.getIamPolicy", "resourcemanager.projects.setIamPolicy"}},
{"t6-control-prefix", []string{"storage.buckets.getIamPolicy", "storage.buckets.setIamPolicy"}},
{"t7-nightly-wipe", []string{"cloudscheduler.jobs.create", "cloudscheduler.jobs.get", "iam.serviceAccounts.actAs", "run.jobs.create", "run.jobs.get", "run.jobs.run"}},
{"t8-isolation-gate", []string{"cloudscheduler.jobs.get", "compute.firewalls.get", "compute.networks.get", "compute.routers.get", "compute.subnetworks.get", "iam.roles.get", "iam.serviceAccounts.get", "iam.serviceAccounts.getIamPolicy", "resourcemanager.projects.getIamPolicy", "run.jobs.get", "storage.buckets.get", "storage.buckets.getIamPolicy"}},
}

func fixturePlan(t *testing.T) bootstrap.CompiledPlan {
t.Helper()
request := bootstrap.CompileRequest{
Configuration: fixtureConfiguration(t), Preflight: fixturePreflight(t), PlanID: fixturePlanID,
CreatedAt: fixtureNow, ExpiresAt: fixtureNow.Add(time.Hour),
LocalPolicyHash: repeatHex("a"), ApprovedPolicyHash: repeatHex("a"),
Pricing: bootstrap.PricingEvidence{
MachineType: "e2-medium", Region: fixtureRegion, Zone: fixtureZone, GuestCPUs: 2, MemoryMiB: 4096,
DiskGiB: 100, Instances: 3, LifetimeSeconds: int64((8 * time.Hour) / time.Second), EstimatedRunMicros: 5_000_000,
Currency: "USD", PriceTableDate: "2026-09-09", Schema: bootstrap.PricingSchemaV1,
ObservedAt: fixtureNow.Add(-time.Minute), ValidUntil: fixtureNow.Add(4 * time.Minute),
},
}
request.Pricing.Revision = fixtureRevision(t, request.Pricing)
grants := make([]bootstrap.PermissionGrant, 0)
for _, step := range fixturePermissions {
for _, permission := range step.permissions {
grants = append(grants, bootstrap.PermissionGrant{StepID: step.step, Identity: domain.IdentityHuman, Permission: permission, Granted: true})
}
}
request.Permissions = bootstrap.PermissionEvidence{
Account: fixtureAccount, Project: fixtureProject, Schema: bootstrap.PermissionEvidenceSchemaV1,
ObservedAt: fixtureNow.Add(-time.Minute), ValidUntil: fixtureNow.Add(4 * time.Minute), Grants: grants,
}
request.Permissions.Revision = fixtureRevision(t, request.Permissions)
plan, err := bootstrap.Compile(request)
if err != nil {
t.Fatalf("bootstrap.Compile() unexpected error: %v", err)
}
return plan
}

func fixtureRevision(t *testing.T, value any) string {
t.Helper()
encoded, err := json.Marshal(value)
if err != nil {
t.Fatalf("json.Marshal(revision input) unexpected error: %v", err)
}
digest := sha256.Sum256(encoded)
return hex.EncodeToString(digest[:])
}

func fixtureConfiguration(t *testing.T) config.HarnessConfiguration {
t.Helper()
encoded, err := os.ReadFile("../config/testdata/manifest-v1alpha1.yaml")
if err != nil {
t.Fatalf("read manifest fixture: %v", err)
}
envelope, err := config.DecodeManifestEnvelope(encoded)
if err != nil {
t.Fatalf("config.DecodeManifestEnvelope() unexpected error: %v", err)
}
var manifest map[string]any
if err := json.Unmarshal(envelope.JSON(), &manifest); err != nil {
t.Fatalf("json.Unmarshal(manifest) unexpected error: %v", err)
}
metadata := manifest["metadata"].(map[string]any)
metadata["name"] = "disposable-test"
metadata["class"] = "disposable"
spec := manifest["spec"].(map[string]any)
spec["testIsolation"] = map[string]any{
"namePrefix": config.TestResourcePrefix,
"labels": map[string]any{"managed-by": "ctrldb", "environment": "disposable", "purpose": "test"},
"operatorServiceAccount": "ctrldb-test-operator@example-project.iam.gserviceaccount.com",
"destructiveServiceAccount": "ctrldb-test-destructive@example-project.iam.gserviceaccount.com",
"network": map[string]any{"vpc": "ctrldb-test-vpc", "subnet": "ctrldb-test-subnet", "cidr": "10.40.0.0/24", "nat": "ctrldb-test-nat"},
"ciPrincipal": "principalSet://iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/example-pool/attribute.repository/example-org/ctrldb",
"caps": map[string]any{"maxMachineType": "e2-medium", "maxDiskGiB": 100, "maxInstances": 3, "maxLifetime": "8h", "maxEstimatedUSDPerRun": 25},
"monitoringTests": "manual-only",
}
spec["host"].(map[string]any)["serviceAccount"] = "ctrldb-test-vm@example-project.iam.gserviceaccount.com"
reconciler := spec["reconciler"].(map[string]any)
reconciler["schedulerJob"] = "ctrldb-test-wipe-schedule"
reconciler["runJob"] = "ctrldb-test-wipe"
reconciler["serviceAccount"] = "ctrldb-test-wipe@example-project.iam.gserviceaccount.com"
rewritten, err := json.Marshal(manifest)
if err != nil {
t.Fatalf("json.Marshal(manifest) unexpected error: %v", err)
}
document, err := config.DecodeManifest(rewritten)
if err != nil {
t.Fatalf("config.DecodeManifest() unexpected error: %v", err)
}
configuration, err := config.HarnessConfigurationFromManifest(document)
if err != nil {
t.Fatalf("config.HarnessConfigurationFromManifest() unexpected error: %v", err)
}
return configuration
}

func fixturePreflight(t *testing.T) observation.HarnessPreflight {
t.Helper()
services := make([]observation.APIService, 0)
for _, service := range bootstrap.RequiredAPIs() {
services = append(services, observation.APIService{Name: service, State: observation.APIEnabled})
}
seed := observation.Seed{
Account: fixtureAccount, Project: fixtureProject, Region: fixtureRegion, Zone: fixtureZone,
GcloudVersion: observation.SupportedGcloudVersion, CompletenessPolicy: observation.GcloudCompletenessPolicy,
ObservedAt: fixtureNow.Add(-time.Minute), ValidUntil: fixtureNow.Add(4 * time.Minute),
Schemas: observation.RequiredSchemas(),
Regions: []observation.Region{{Name: fixtureRegion, Availability: observation.AvailabilityUp, ProviderID: "projects/" + fixtureProject + "/regions/" + fixtureRegion}},
Zones: []observation.Zone{{Name: fixtureZone, Region: fixtureRegion, Availability: observation.AvailabilityUp, ProviderID: "projects/" + fixtureProject + "/zones/" + fixtureZone}},
MachineTypes: []observation.MachineType{{
Name: "e2-medium", Zone: fixtureZone, GuestCPUs: 2, MemoryMiB: 4096,
ProviderID: "projects/" + fixtureProject + "/zones/" + fixtureZone + "/machineTypes/e2-medium",
}},
APIs: services, Exhaustive: true,
}
preflight, err := observation.NewHarnessPreflight(seed)
if err != nil {
t.Fatalf("observation.NewHarnessPreflight() unexpected error: %v", err)
}
return preflight
}

func fixtureJournalEntry(plan bootstrap.CompiledPlan, recordedAt time.Time) domain.JournalEntry {
return domain.JournalEntry{
Schema: domain.JournalSchemaV1, OperationID: fixtureOperationID, PlanID: plan.Plan().PlanID,
ContractHash: plan.ExecutionContract().Digest(), Sequence: 1, Kind: domain.JournalEntryTransition,
RecordedAt: recordedAt, OperationState: domain.OperationDiscover,
}
}

func fixtureSeed(t *testing.T) EnvelopeSeed {
t.Helper()
plan := fixturePlan(t)
approval, err := NewApprovalProof(plan, fixtureAccount, fixtureNow.Add(time.Minute), fixtureNow.Add(30*time.Minute))
if err != nil {
t.Fatalf("NewApprovalProof() unexpected error: %v", err)
}
return EnvelopeSeed{
Plan: plan, OperationID: fixtureOperationID, Approval: approval,
FirstJournalEntry: fixtureJournalEntry(plan, fixtureNow.Add(2*time.Minute)), SealedAt: fixtureNow.Add(2 * time.Minute),
}
}

func fixtureEnvelope(t *testing.T) BootstrapEnvelopeV1 {
t.Helper()
envelope, err := SealBootstrapEnvelope(fixtureSeed(t))
if err != nil {
t.Fatalf("SealBootstrapEnvelope() unexpected error: %v", err)
}
return envelope
}

func fixtureStateDirectory(t *testing.T) StateDirectory {
t.Helper()
path := t.TempDir()
if err := os.Chmod(path, 0o700); err != nil {
t.Fatalf("chmod state directory: %v", err)
}
directory, err := NewStateDirectory(path)
if err != nil {
t.Fatalf("NewStateDirectory() unexpected error: %v", err)
}
return directory
}

func repeatHex(character string) string {
result := make([]byte, 64)
for index := range result {
result[index] = character[0]
}
return string(result)
}
Loading