//go:build linux package host import ( "bytes" "context" "crypto/ed25519" "encoding/json" "flag" "io" "github.com/carlosframework/platform/internal/claimactivation" "github.com/carlosframework/platform/internal/hostactors" "github.com/carlosframework/platform/internal/hostfloor" "github.com/carlosframework/platform/internal/publicrelease" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/version" "runtime" ) func runSystemClaims(args []string, out io.Writer) error { if len(args) == 1 && (args[0] == "help" || args[0] == "--help" || args[0] == "-h") { _, err := io.WriteString(out, activationClaimsUsage) return err } if len(args) == 0 { return errActivation } fs := flag.NewFlagSet("system claims "+args[0], flag.ContinueOnError) fs.SetOutput(io.Discard) consoleID := fs.String("console-id", "", "") enableRuntime := fs.Bool("enable-console-runtime", false, "") dry := fs.Bool("dry-run", false, "") planPath := fs.String("plan", "", "") keyPath := fs.String("signing-key", "", "") pubValue := fs.String("public-key", "", "") region := fs.String("aws-region", "", "") rosterPath := fs.String("roster", "", "") revision := fs.Uint64("expect-revision", 0, "") box := fs.String("box", "", "") enrollment := fs.String("enrollment", "", "") source := fs.String("source", "", "") bucket := fs.String("bucket", "", "") role := fs.String("role", "", "") outPath := fs.String("out", "", "") identityPath := fs.String("identity", "", "") upgrade := fs.Bool("upgrade", false, "") from := fs.String("from", "", "") if err := fs.Parse(args[1:]); err != nil || len(fs.Args()) != 0 { return errActivation } if upgradeFlagsRefused(args[0], fs, *upgrade, *from) { return errActivation } ctx := context.Background() encode := func(value any) error { e := json.NewEncoder(out); e.SetIndent("", " "); return e.Encode(value) } switch args[0] { case "inspect": if *bucket == "" || *region == "" { return errActivation } pub, err := release.ParsePublicKey(*pubValue, "public-key") if err != nil { return errActivation } store, err := release.NewDeploymentS3StoreIn(ctx, *bucket, *region) if err != nil { return err } roster, err := (hostactors.Directory{Store: store, PublicKey: pub, Bucket: *bucket}).Read(ctx) if err != nil { return err } wire, tag, err := store.Get(ctx, hostactors.Key) if err != nil || tag != roster.ETag { return errActivation } return encode(struct { Roster hostactors.Roster `json:"roster"` ETag string `json:"etag"` SignedRoster json.RawMessage `json:"signed_roster"` }{roster.Roster, roster.ETag, wire}) case "console-report": if *consoleID == "" || *bucket == "" || *region == "" || *revision == 0 { return errActivation } pub, err := release.ParsePublicKey(*pubValue, "public-key") if err != nil { return errActivation } store, err := release.NewDeploymentS3StoreIn(ctx, *bucket, *region) if err != nil { return err } report, err := publishActivationConsoleReport(ctx, store, pub, *bucket, *consoleID, *revision, *dry) if err != nil { return err } localRaw, err := activationRead("/etc/carlos/host-actor.json", false) if err != nil { return err } var local hostfloor.Enrollment if json.Unmarshal(localRaw, &local) != nil { return errActivation } return encode(map[string]any{"dry_run": *dry, "bucket": *bucket, "key": "status/boxes/" + local.Box + ".json", "expected_roster_revision": *revision, "console_process": report}) case "console-config": if *consoleID == "" || *bucket == "" || *revision == 0 || *rosterPath == "" { return errActivation } pub, err := release.ParsePublicKey(*pubValue, "public-key") if err != nil { return errActivation } raw, err := activationRead(*rosterPath, false) if err != nil { return err } var roster hostactors.Roster if release.VerifyRootEnvelope(pub, raw, &roster) != nil || roster.Validate(*bucket) != nil || roster.Revision != *revision { return errActivation } localRaw, err := activationRead("/etc/carlos/host-actor.json", false) if err != nil { return err } var local hostfloor.Enrollment if json.Unmarshal(localRaw, &local) != nil { return errActivation } env, err := activationRead("/etc/carlos/host.env", false) if err != nil { return err } content, err := activationConsoleEnvironment(roster, local, env, *consoleID, *enableRuntime) if err != nil { return err } policyRaw, err := activationRead("/opt/carlos/system/guard/policy.json", false) if err != nil { return err } var policy hostfloor.Policy if json.Unmarshal(policyRaw, &policy) != nil || policy.Mode == "unpublished" { return errActivation } floor, err := policy.Verify(pub, *bucket) if err != nil { return err } if *enableRuntime && floor.Digest != roster.FloorSHA256 { return errActivation } consolePath, err := activationRead("/opt/carlos/system/guard/console-ready.json", false) if err != nil { return err } guard, err := activationRead("/etc/systemd/system/console.service.d/90-host-floor.conf", false) if err != nil { return err } if string(guard) != hostfloor.ConsoleGuardDropIn(string(consolePath)) { return errActivation } if err = installActivationConsole("/etc/carlos/claim-console.env", "/etc/systemd/system/console.service.d", content, *dry, productionSysOps().systemctl); err != nil { return err } return encode(map[string]any{"dry_run": *dry, "console_id": *consoleID, "environment": string(content), "path": "/etc/carlos/claim-console.env", "drop_in": "/etc/systemd/system/console.service.d/91-claim-runtime.conf", "reload": true, "start": false}) case "run": if *planPath == "" || *keyPath == "" || *pubValue == "" || *region == "" { return errActivation } raw, err := activationRead(*planPath, false) if err != nil { return err } var plan claimactivation.Plan dec := json.NewDecoder(bytes.NewReader(raw)) dec.DisallowUnknownFields() if dec.Decode(&plan) != nil || dec.Decode(new(any)) != io.EOF { return errActivation } keyRaw, err := activationRead(*keyPath, true) if err != nil { return err } key, err := release.ParseSigningKey(string(keyRaw), *keyPath) if err != nil { return errActivation } pub, err := release.ParsePublicKey(*pubValue, "public-key") if err != nil { return errActivation } if !bytes.Equal(key.Public().(ed25519.PublicKey), pub) { return errActivation } // sign-policy and publish-identity produce a file, written to --out // byte for byte; every other phase takes no --out. produces := plan.Phase == "sign-policy" || plan.Phase == "publish-identity" if produces != (*outPath != "") { return errActivation } store, err := release.NewDeploymentS3StoreIn(ctx, plan.Bucket, *region) if err != nil { return err } runner := activationRunner(store, pub, plan.Bucket, key) var result claimactivation.Result if produces { // Reserved before the phase can write to the bucket (F4). out, e := reserveActivationOutput(*outPath, *dry) if e != nil { return e } result, err = runProducingPhase(ctx, runner, &putAttempts{Store: store}, plan, out, *dry) } else { result, err = runner.Run(ctx, plan, *dry) } if err != nil { return err } return encode(struct { claimactivation.Result Out string `json:"out,omitempty"` }{result, *outPath}) case "evidence-key": if *rosterPath == "" || *revision == 0 || *bucket == "" || *box == "" || *enrollment == "" { return errActivation } pub, err := release.ParsePublicKey(*pubValue, "public-key") if err != nil { return errActivation } raw, err := activationRead(*rosterPath, false) if err != nil { return err } var roster hostactors.Roster if release.VerifyRootEnvelope(pub, raw, &roster) != nil || roster.Validate(*bucket) != nil || roster.Revision != *revision { return errActivation } actor, ok := roster.Boxes[*box] if !ok || actor.Enrollment != *enrollment || actor.State != "active" { return errActivation } localRaw, err := activationRead("/etc/carlos/host-actor.json", false) if err != nil { return err } var local hostfloor.Enrollment if json.Unmarshal(localRaw, &local) != nil || local.Box != *box || local.Enrollment != *enrollment || local.InstanceID != actor.Cloud.InstanceID { return errActivation } proof, err := installActivationEvidenceKey("/etc/carlos/host-evidence.key", hostactors.CensusKeyBinding{Bucket: *bucket, Box: *box, Enrollment: *enrollment, PublicKey: actor.CensusPublicKey}, *dry) if err != nil { return err } return encode(struct { DryRun bool `json:"dry_run"` Path string `json:"path"` Proof activationKeyProof `json:"key_proof"` }{*dry, "/etc/carlos/host-evidence.key", proof}) case "fence-credentials": if *source == "" || *bucket == "" || *region == "" || *role == "" { return errActivation } target := "/etc/carlos/fence-writer/credentials.json" if err := installActivationFenceCredentials(*source, target, activationFenceScope{Bucket: *bucket, Region: *region, Role: *role}, *dry); err != nil { return err } return encode(map[string]any{"dry_run": *dry, "source": *source, "target": target, "mode": "0600", "bucket": *bucket, "aws_region": *region, "role": *role}) case "install-console": if *identityPath == "" || *source == "" || *bucket == "" || *region == "" { return errActivation } pub, err := release.ParsePublicKey(*pubValue, "public-key") if err != nil { return errActivation } store, err := release.NewDeploymentS3StoreIn(ctx, *bucket, *region) if err != nil { return err } tag, sha, got, err := runInstallConsole(ctx, store, pub, *identityPath, *source, consoleInstallDir, version.Build, runtime.GOOS+"-"+runtime.GOARCH, *from, *dry) if err != nil { return err } result := map[string]any{"dry_run": *dry, "release_tag": tag, "sha256": sha, "source": *source, "path": got.Path, "previous": got.Previous, "already_installed": got.Already, "mode": "0755"} if *upgrade { result["upgraded"], result["from"], result["previous_sha"] = true, got.From, got.PreviousSHA } return encode(result) case "install-control": if err := verifyOfflineRootMigration(ctx); err != nil { return err } if err := installActivationControl("/etc/carlos/host.env", "/etc/systemd/system", *dry, productionSysOps().systemctl); err != nil { return err } return encode(map[string]any{"dry_run": *dry, "source": "/etc/carlos/host.env", "target": "/etc/systemd/system/" + claimControlUnit, "reload": true, "start": false}) default: return errActivation } } // upgradeFlagsRefused reports whether parsed flags misuse --upgrade and // --from: they belong to install-console and come together, so anything // else is refused as an unknown flag was before them. func upgradeFlagsRefused(command string, fs *flag.FlagSet, upgrade bool, from string) bool { set := false fs.Visit(func(f *flag.Flag) { set = set || f.Name == "upgrade" || f.Name == "from" }) return set && (command != "install-console" || !upgrade || from == "") } // activationRunner is claims run's runner. Its sums key is the vendor's // release key that the carlos binary embeds (publicrelease.Default, the same // key update.go's cliUpdatePubKey holds), so publish-identity accepts only a // checksum list the vendor signed. func activationRunner(store release.Store, pub ed25519.PublicKey, bucket string, key ed25519.PrivateKey) claimactivation.Runner { return claimactivation.Runner{Directory: hostactors.Directory{Store: store, PublicKey: pub, Bucket: bucket}, Signer: release.RootSigner(key), SumsKey: publicrelease.Default().PubKey} }