Merge branch 'feat-launchctx-module' into main
16 files changed,
+344
−254
Makefile+1 −0go.mod+4 −0internal/controlname/names.go+5 −25internal/maintenance/launch.go+8 −212internal/maintenanceguard/types.go+3 −15launchctx/controlname/names.go+28 −0launchctx/go.mod+3 −0launchctx/launch.go+221 −0{internal/maintenance => launchctx}/launch_test.go+1 −1{internal/maintenance => launchctx}/launch_utf8.go+1 −1launchctx/mode.go+17 −0launchctx/mode_test.go+31 −0{internal/maintenance => launchctx}/testdata/generate-launch-vector.py+0 −0{internal/maintenance => launchctx}/testdata/launch-v2-binding.json+0 −0{internal/maintenance => launchctx}/testdata/launch-v2.json+0 −0launchctx/wireid/id.go+21 −0
CI passed — run details
diff --git a/Makefile b/Makefile| index 5a963c1e4..8228319ee 100644 |
| --- a/Makefile |
| +++ b/Makefile |
| @@ -12,6 +12,7 @@ GOARCH ?= |
| # The 30m timeout is above Go's 10m default because the root package's e2e |
| # tests compile the carlos binary many times over. |
| test: |
| + cd launchctx && go vet ./... && go test ./... -count=1 |
| go vet ./... |
| @fmt_out=$$(gofmt -l .); if [ -n "$$fmt_out" ]; then echo "gofmt needed:"; echo "$$fmt_out"; exit 1; fi; echo "gofmt -l .: clean" |
| go test ./... -count=1 -timeout 30m |
diff --git a/go.mod b/go.mod| index 698b9d75f..07f91eef6 100644 |
| --- a/go.mod |
| +++ b/go.mod |
| @@ -78,3 +78,7 @@ require ( |
| modernc.org/mathutil v1.7.1 // indirect |
| modernc.org/memory v1.11.0 // indirect |
| ) |
| + |
| +require amadan.net/carlos/platform.git/launchctx v0.0.0 |
| + |
| +replace amadan.net/carlos/platform.git/launchctx => ./launchctx |
diff --git a/internal/controlname/names.go b/internal/controlname/names.go| index 51c0876fa..a6f51d4a5 100644 |
| --- a/internal/controlname/names.go |
| +++ b/internal/controlname/names.go |
| @@ -1,28 +1,8 @@ |
| -// Package controlname validates identifiers used as control-plane key segments. |
| package controlname |
| -import ( |
| - "regexp" |
| - "strings" |
| -) |
| +import names "amadan.net/carlos/platform.git/launchctx/controlname" |
| -var ( |
| - account = regexp.MustCompile(`^[a-z]{3,16}$`) |
| - app = regexp.MustCompile(`^[a-z][a-z0-9-]{0,31}$`) |
| - label = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$`) |
| -) |
| - |
| -func ValidAccount(s string) bool { return account.MatchString(s) } |
| -func ValidApp(s string) bool { return app.MatchString(s) } |
| -func ValidLabel(s string) bool { return label.MatchString(s) } |
| -func ValidHostShape(host string) bool { |
| - if host == "" || len(host) > 253 { |
| - return false |
| - } |
| - for _, part := range strings.Split(host, ".") { |
| - if !ValidLabel(part) { |
| - return false |
| - } |
| - } |
| - return true |
| -} |
| +func ValidAccount(s string) bool { return names.ValidAccount(s) } |
| +func ValidApp(s string) bool { return names.ValidApp(s) } |
| +func ValidLabel(s string) bool { return names.ValidLabel(s) } |
| +func ValidHostShape(s string) bool { return names.ValidHostShape(s) } |
diff --git a/internal/maintenance/launch.go b/internal/maintenance/launch.go| index edc48b0d9..36b5b608c 100644 |
| --- a/internal/maintenance/launch.go |
| +++ b/internal/maintenance/launch.go |
| @@ -1,221 +1,17 @@ |
| package maintenance |
| import ( |
| - "bytes" |
| - "crypto/sha256" |
| - "encoding/hex" |
| - "encoding/json" |
| - "errors" |
| + "amadan.net/carlos/platform.git/launchctx" |
| "io" |
| - "strings" |
| - "time" |
| - "unicode/utf8" |
| - |
| - "github.com/carlosframework/platform/internal/controlname" |
| - "github.com/carlosframework/platform/internal/maintenanceguard" |
| ) |
| -const MaxLaunchBytes = 8192 |
| - |
| -var ErrLaunch = errors.New("maintenance: invalid private launch context") |
| - |
| -// LaunchContext is private transport metadata, never proof of approval, |
| -// exclusion, lease ownership or a trusted worker invocation. |
| -type LaunchContext struct { |
| - Version int `json:"version"` |
| - Account string `json:"account"` |
| - App string `json:"app"` |
| - Host string `json:"host"` |
| - RequestID string `json:"request_id"` |
| - AttemptID string `json:"attempt_id"` |
| - RequestDigest string `json:"request_digest"` |
| - RecordIdentity string `json:"record_identity"` |
| - ArtifactDigest string `json:"artifact_digest"` |
| - EnvironmentEnvelopeDigest string `json:"environment_envelope_digest"` |
| - GuardGeneration int64 `json:"guard_generation"` |
| - InputSHA256 string `json:"input_sha256"` |
| - DatabasePath string `json:"database_path"` |
| - DatabaseDevice uint64 `json:"database_device"` |
| - DatabaseInode uint64 `json:"database_inode"` |
| - ExecutionDeadline string `json:"execution_deadline"` |
| - ExecutionKind string `json:"execution_kind,omitempty"` |
| - OperationID string `json:"operation_id,omitempty"` |
| - ExpectedStateSHA256 string `json:"expected_state_sha256,omitempty"` |
| - ClosedCheckpoint *ClosedCheckpoint `json:"closed_checkpoint,omitempty"` |
| -} |
| - |
| -type ClosedCheckpoint struct { |
| - Version int `json:"version"` |
| - PreparationID string `json:"preparation_id"` |
| - CompletedAt string `json:"completed_at"` |
| - MainLength int64 `json:"main_length"` |
| - MainSHA256 string `json:"main_sha256"` |
| - EvidenceSHA256 string `json:"evidence_sha256"` |
| -} |
| - |
| -// ReadLaunch requires EOF and consumes at most the bound plus one byte. |
| -// The worker/adapter must separately bound a reader that can block. |
| -func ReadLaunch(r io.Reader) (LaunchContext, error) { |
| - if r == nil { |
| - return LaunchContext{}, ErrLaunch |
| - } |
| - raw, err := io.ReadAll(io.LimitReader(r, MaxLaunchBytes+1)) |
| - if err != nil { |
| - return LaunchContext{}, ErrLaunch |
| - } |
| - return DecodeLaunch(raw) |
| -} |
| - |
| -func DecodeLaunch(raw []byte) (LaunchContext, error) { |
| - fail := func() (LaunchContext, error) { return LaunchContext{}, ErrLaunch } |
| - if len(raw) == 0 || len(raw) > MaxLaunchBytes || !utf8.Valid(raw) || !validSurrogates(raw) { |
| - return fail() |
| - } |
| - fields, err := launchObject(raw) |
| - if err != nil { |
| - return fail() |
| - } |
| - var version int |
| - if json.Unmarshal(fields["version"], &version) != nil { |
| - return fail() |
| - } |
| - names := strings.Fields("version account app host request_id attempt_id request_digest record_identity artifact_digest environment_envelope_digest guard_generation input_sha256 database_path database_device database_inode execution_deadline") |
| - switch version { |
| - case 1: |
| - case 2: |
| - names = append(names, strings.Fields("execution_kind operation_id expected_state_sha256 closed_checkpoint")...) |
| - cp, err := launchObject(fields["closed_checkpoint"]) |
| - if err != nil || !exactLaunchFields(cp, strings.Fields("version preparation_id completed_at main_length main_sha256 evidence_sha256")) { |
| - return fail() |
| - } |
| - default: |
| - return fail() |
| - } |
| - if !exactLaunchFields(fields, names) { |
| - return fail() |
| - } |
| - var c LaunchContext |
| - if json.Unmarshal(raw, &c) != nil || !c.valid() { |
| - return fail() |
| - } |
| - return c, nil |
| -} |
| +const MaxLaunchBytes = launchctx.MaxLaunchBytes |
| -func EncodeLaunch(c LaunchContext) ([]byte, error) { |
| - if !c.valid() { |
| - return nil, ErrLaunch |
| - } |
| - raw, err := json.Marshal(c) |
| - if err != nil || len(raw) > MaxLaunchBytes { |
| - return nil, ErrLaunch |
| - } |
| - return raw, nil |
| -} |
| +var ErrLaunch = launchctx.ErrLaunch |
| -func launchObject(raw []byte) (map[string]json.RawMessage, error) { |
| - dec := json.NewDecoder(bytes.NewReader(raw)) |
| - tok, err := dec.Token() |
| - if err != nil || tok != json.Delim('{') { |
| - return nil, ErrLaunch |
| - } |
| - out := map[string]json.RawMessage{} |
| - for dec.More() { |
| - tok, err = dec.Token() |
| - if err != nil { |
| - return nil, ErrLaunch |
| - } |
| - key, ok := tok.(string) |
| - if !ok { |
| - return nil, ErrLaunch |
| - } |
| - if _, exists := out[key]; exists { |
| - return nil, ErrLaunch |
| - } |
| - var value json.RawMessage |
| - if dec.Decode(&value) != nil || bytes.Equal(bytes.TrimSpace(value), []byte("null")) { |
| - return nil, ErrLaunch |
| - } |
| - out[key] = value |
| - } |
| - tok, err = dec.Token() |
| - if err != nil || tok != json.Delim('}') { |
| - return nil, ErrLaunch |
| - } |
| - if _, err = dec.Token(); err != io.EOF { |
| - return nil, ErrLaunch |
| - } |
| - return out, nil |
| -} |
| -func exactLaunchFields(fields map[string]json.RawMessage, names []string) bool { |
| - if len(fields) != len(names) { |
| - return false |
| - } |
| - for _, name := range names { |
| - if _, ok := fields[name]; !ok { |
| - return false |
| - } |
| - } |
| - return true |
| -} |
| -func canonicalStamp(s string) (time.Time, bool) { |
| - t, err := time.Parse(time.RFC3339Nano, s) |
| - return t, err == nil && len(s) <= 30 && t.UTC().Format(time.RFC3339Nano) == s |
| -} |
| -func (c LaunchContext) valid() bool { |
| - if !controlname.ValidAccount(c.Account) || !controlname.ValidApp(c.App) || !controlname.ValidHostShape(c.Host) { |
| - return false |
| - } |
| - for _, id := range []string{c.RequestID, c.AttemptID} { |
| - if !maintenanceguard.ValidID(id) { |
| - return false |
| - } |
| - } |
| - for _, digest := range []string{c.RequestDigest, c.RecordIdentity, c.ArtifactDigest, c.EnvironmentEnvelopeDigest, c.InputSHA256} { |
| - if !maintenanceguard.ValidDigest(digest) { |
| - return false |
| - } |
| - } |
| - if c.GuardGeneration <= 0 || c.DatabaseDevice == 0 || c.DatabaseInode == 0 { |
| - return false |
| - } |
| - if len(c.DatabasePath) == 0 || len(c.DatabasePath) > 1024 || c.DatabasePath[0] != '/' || strings.ContainsRune(c.DatabasePath, 0) || !utf8.ValidString(c.DatabasePath) { |
| - return false |
| - } |
| - deadline, ok := canonicalStamp(c.ExecutionDeadline) |
| - if !ok { |
| - return false |
| - } |
| - switch c.Version { |
| - case 1: |
| - return c.ExecutionKind == "" && c.OperationID == "" && c.ExpectedStateSHA256 == "" && c.ClosedCheckpoint == nil |
| - case 2: |
| - if c.ExecutionKind != "compatibility-check" || !maintenanceguard.ValidID(c.OperationID) || !maintenanceguard.ValidDigest(c.ExpectedStateSHA256) || c.ClosedCheckpoint == nil { |
| - return false |
| - } |
| - p := c.ClosedCheckpoint |
| - completed, ok := canonicalStamp(p.CompletedAt) |
| - if !ok || completed.After(deadline) || p.Version != 1 || !maintenanceguard.ValidID(p.PreparationID) || p.MainLength <= 0 || p.MainLength > 268435456 || !maintenanceguard.ValidDigest(p.MainSHA256) || !maintenanceguard.ValidDigest(p.EvidenceSHA256) { |
| - return false |
| - } |
| - return p.EvidenceSHA256 == c.checkpointDigest() |
| - } |
| - return false |
| -} |
| +type LaunchContext = launchctx.LaunchContext |
| +type ClosedCheckpoint = launchctx.ClosedCheckpoint |
| -// CheckpointDigest correlates private bindings; it does not certify that a |
| -// checkpoint, writer exclusion or read-only namespace actually exists. |
| -func (c LaunchContext) CheckpointDigest() (string, error) { |
| - if c.Version != 2 || c.ClosedCheckpoint == nil { |
| - return "", ErrLaunch |
| - } |
| - return c.checkpointDigest(), nil |
| -} |
| -func (c LaunchContext) checkpointBytes() []byte { |
| - p := c.ClosedCheckpoint |
| - raw, _ := json.Marshal([]any{"carlos/closed-checkpoint/v1", c.Account, c.App, c.Host, c.OperationID, c.RequestID, c.AttemptID, c.RequestDigest, c.RecordIdentity, c.ArtifactDigest, c.EnvironmentEnvelopeDigest, c.GuardGeneration, c.InputSHA256, c.ExpectedStateSHA256, c.DatabasePath, c.DatabaseDevice, c.DatabaseInode, c.ExecutionDeadline, p.PreparationID, p.CompletedAt, p.MainLength, p.MainSHA256}) |
| - return raw |
| -} |
| -func (c LaunchContext) checkpointDigest() string { |
| - sum := sha256.Sum256(c.checkpointBytes()) |
| - return hex.EncodeToString(sum[:]) |
| -} |
| +func ReadLaunch(r io.Reader) (LaunchContext, error) { return launchctx.ReadLaunch(r) } |
| +func DecodeLaunch(raw []byte) (LaunchContext, error) { return launchctx.DecodeLaunch(raw) } |
| +func EncodeLaunch(c LaunchContext) ([]byte, error) { return launchctx.EncodeLaunch(c) } |
diff --git a/internal/maintenanceguard/types.go b/internal/maintenanceguard/types.go| index a8d6911fb..ac6f9a6fe 100644 |
| --- a/internal/maintenanceguard/types.go |
| +++ b/internal/maintenanceguard/types.go |
| @@ -3,8 +3,8 @@ |
| package maintenanceguard |
| import ( |
| + "amadan.net/carlos/platform.git/launchctx/wireid" |
| "crypto/sha256" |
| - "encoding/base64" |
| "encoding/binary" |
| "encoding/hex" |
| "encoding/json" |
| @@ -29,20 +29,8 @@ var ( |
| contractRE = regexp.MustCompile(`^[a-z][a-z0-9]*([.-][a-z0-9]+)*$`) |
| ) |
| -func ValidID(s string) bool { |
| - if len(s) != 43 { |
| - return false |
| - } |
| - b, e := base64.RawURLEncoding.Strict().DecodeString(s) |
| - return e == nil && len(b) == 32 && base64.RawURLEncoding.EncodeToString(b) == s |
| -} |
| -func ValidDigest(s string) bool { |
| - if len(s) != 64 { |
| - return false |
| - } |
| - b, e := hex.DecodeString(s) |
| - return e == nil && len(b) == 32 && hex.EncodeToString(b) == s |
| -} |
| +func ValidID(s string) bool { return wireid.ValidID(s) } |
| +func ValidDigest(s string) bool { return wireid.ValidDigest(s) } |
| func ValidContract(s string) bool { return len(s) > 0 && len(s) <= 64 && contractRE.MatchString(s) } |
| func ValidContracts(set []string) bool { |
| if len(set) > MaxContracts { |
diff --git a/launchctx/controlname/names.go b/launchctx/controlname/names.go| new file mode 100644 |
| index 000000000..51c0876fa |
| --- /dev/null |
| +++ b/launchctx/controlname/names.go |
| @@ -0,0 +1,28 @@ |
| +// Package controlname validates identifiers used as control-plane key segments. |
| +package controlname |
| + |
| +import ( |
| + "regexp" |
| + "strings" |
| +) |
| + |
| +var ( |
| + account = regexp.MustCompile(`^[a-z]{3,16}$`) |
| + app = regexp.MustCompile(`^[a-z][a-z0-9-]{0,31}$`) |
| + label = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$`) |
| +) |
| + |
| +func ValidAccount(s string) bool { return account.MatchString(s) } |
| +func ValidApp(s string) bool { return app.MatchString(s) } |
| +func ValidLabel(s string) bool { return label.MatchString(s) } |
| +func ValidHostShape(host string) bool { |
| + if host == "" || len(host) > 253 { |
| + return false |
| + } |
| + for _, part := range strings.Split(host, ".") { |
| + if !ValidLabel(part) { |
| + return false |
| + } |
| + } |
| + return true |
| +} |
diff --git a/launchctx/go.mod b/launchctx/go.mod| new file mode 100644 |
| index 000000000..bf98fb41e |
| --- /dev/null |
| +++ b/launchctx/go.mod |
| @@ -0,0 +1,3 @@ |
| +module amadan.net/carlos/platform.git/launchctx |
| + |
| +go 1.25.0 |
diff --git a/launchctx/launch.go b/launchctx/launch.go| new file mode 100644 |
| index 000000000..76b602b80 |
| --- /dev/null |
| +++ b/launchctx/launch.go |
| @@ -0,0 +1,221 @@ |
| +package launchctx |
| + |
| +import ( |
| + "bytes" |
| + "crypto/sha256" |
| + "encoding/hex" |
| + "encoding/json" |
| + "errors" |
| + "io" |
| + "strings" |
| + "time" |
| + "unicode/utf8" |
| + |
| + "amadan.net/carlos/platform.git/launchctx/controlname" |
| + "amadan.net/carlos/platform.git/launchctx/wireid" |
| +) |
| + |
| +const MaxLaunchBytes = 8192 |
| + |
| +var ErrLaunch = errors.New("maintenance: invalid private launch context") |
| + |
| +// LaunchContext is private transport metadata, never proof of approval, |
| +// exclusion, lease ownership or a trusted worker invocation. |
| +type LaunchContext struct { |
| + Version int `json:"version"` |
| + Account string `json:"account"` |
| + App string `json:"app"` |
| + Host string `json:"host"` |
| + RequestID string `json:"request_id"` |
| + AttemptID string `json:"attempt_id"` |
| + RequestDigest string `json:"request_digest"` |
| + RecordIdentity string `json:"record_identity"` |
| + ArtifactDigest string `json:"artifact_digest"` |
| + EnvironmentEnvelopeDigest string `json:"environment_envelope_digest"` |
| + GuardGeneration int64 `json:"guard_generation"` |
| + InputSHA256 string `json:"input_sha256"` |
| + DatabasePath string `json:"database_path"` |
| + DatabaseDevice uint64 `json:"database_device"` |
| + DatabaseInode uint64 `json:"database_inode"` |
| + ExecutionDeadline string `json:"execution_deadline"` |
| + ExecutionKind string `json:"execution_kind,omitempty"` |
| + OperationID string `json:"operation_id,omitempty"` |
| + ExpectedStateSHA256 string `json:"expected_state_sha256,omitempty"` |
| + ClosedCheckpoint *ClosedCheckpoint `json:"closed_checkpoint,omitempty"` |
| +} |
| + |
| +type ClosedCheckpoint struct { |
| + Version int `json:"version"` |
| + PreparationID string `json:"preparation_id"` |
| + CompletedAt string `json:"completed_at"` |
| + MainLength int64 `json:"main_length"` |
| + MainSHA256 string `json:"main_sha256"` |
| + EvidenceSHA256 string `json:"evidence_sha256"` |
| +} |
| + |
| +// ReadLaunch requires EOF and consumes at most the bound plus one byte. |
| +// The worker/adapter must separately bound a reader that can block. |
| +func ReadLaunch(r io.Reader) (LaunchContext, error) { |
| + if r == nil { |
| + return LaunchContext{}, ErrLaunch |
| + } |
| + raw, err := io.ReadAll(io.LimitReader(r, MaxLaunchBytes+1)) |
| + if err != nil { |
| + return LaunchContext{}, ErrLaunch |
| + } |
| + return DecodeLaunch(raw) |
| +} |
| + |
| +func DecodeLaunch(raw []byte) (LaunchContext, error) { |
| + fail := func() (LaunchContext, error) { return LaunchContext{}, ErrLaunch } |
| + if len(raw) == 0 || len(raw) > MaxLaunchBytes || !utf8.Valid(raw) || !validSurrogates(raw) { |
| + return fail() |
| + } |
| + fields, err := launchObject(raw) |
| + if err != nil { |
| + return fail() |
| + } |
| + var version int |
| + if json.Unmarshal(fields["version"], &version) != nil { |
| + return fail() |
| + } |
| + names := strings.Fields("version account app host request_id attempt_id request_digest record_identity artifact_digest environment_envelope_digest guard_generation input_sha256 database_path database_device database_inode execution_deadline") |
| + switch version { |
| + case 1: |
| + case 2: |
| + names = append(names, strings.Fields("execution_kind operation_id expected_state_sha256 closed_checkpoint")...) |
| + cp, err := launchObject(fields["closed_checkpoint"]) |
| + if err != nil || !exactLaunchFields(cp, strings.Fields("version preparation_id completed_at main_length main_sha256 evidence_sha256")) { |
| + return fail() |
| + } |
| + default: |
| + return fail() |
| + } |
| + if !exactLaunchFields(fields, names) { |
| + return fail() |
| + } |
| + var c LaunchContext |
| + if json.Unmarshal(raw, &c) != nil || !c.valid() { |
| + return fail() |
| + } |
| + return c, nil |
| +} |
| + |
| +func EncodeLaunch(c LaunchContext) ([]byte, error) { |
| + if !c.valid() { |
| + return nil, ErrLaunch |
| + } |
| + raw, err := json.Marshal(c) |
| + if err != nil || len(raw) > MaxLaunchBytes { |
| + return nil, ErrLaunch |
| + } |
| + return raw, nil |
| +} |
| + |
| +func launchObject(raw []byte) (map[string]json.RawMessage, error) { |
| + dec := json.NewDecoder(bytes.NewReader(raw)) |
| + tok, err := dec.Token() |
| + if err != nil || tok != json.Delim('{') { |
| + return nil, ErrLaunch |
| + } |
| + out := map[string]json.RawMessage{} |
| + for dec.More() { |
| + tok, err = dec.Token() |
| + if err != nil { |
| + return nil, ErrLaunch |
| + } |
| + key, ok := tok.(string) |
| + if !ok { |
| + return nil, ErrLaunch |
| + } |
| + if _, exists := out[key]; exists { |
| + return nil, ErrLaunch |
| + } |
| + var value json.RawMessage |
| + if dec.Decode(&value) != nil || bytes.Equal(bytes.TrimSpace(value), []byte("null")) { |
| + return nil, ErrLaunch |
| + } |
| + out[key] = value |
| + } |
| + tok, err = dec.Token() |
| + if err != nil || tok != json.Delim('}') { |
| + return nil, ErrLaunch |
| + } |
| + if _, err = dec.Token(); err != io.EOF { |
| + return nil, ErrLaunch |
| + } |
| + return out, nil |
| +} |
| +func exactLaunchFields(fields map[string]json.RawMessage, names []string) bool { |
| + if len(fields) != len(names) { |
| + return false |
| + } |
| + for _, name := range names { |
| + if _, ok := fields[name]; !ok { |
| + return false |
| + } |
| + } |
| + return true |
| +} |
| +func canonicalStamp(s string) (time.Time, bool) { |
| + t, err := time.Parse(time.RFC3339Nano, s) |
| + return t, err == nil && len(s) <= 30 && t.UTC().Format(time.RFC3339Nano) == s |
| +} |
| +func (c LaunchContext) valid() bool { |
| + if !controlname.ValidAccount(c.Account) || !controlname.ValidApp(c.App) || !controlname.ValidHostShape(c.Host) { |
| + return false |
| + } |
| + for _, id := range []string{c.RequestID, c.AttemptID} { |
| + if !wireid.ValidID(id) { |
| + return false |
| + } |
| + } |
| + for _, digest := range []string{c.RequestDigest, c.RecordIdentity, c.ArtifactDigest, c.EnvironmentEnvelopeDigest, c.InputSHA256} { |
| + if !wireid.ValidDigest(digest) { |
| + return false |
| + } |
| + } |
| + if c.GuardGeneration <= 0 || c.DatabaseDevice == 0 || c.DatabaseInode == 0 { |
| + return false |
| + } |
| + if len(c.DatabasePath) == 0 || len(c.DatabasePath) > 1024 || c.DatabasePath[0] != '/' || strings.ContainsRune(c.DatabasePath, 0) || !utf8.ValidString(c.DatabasePath) { |
| + return false |
| + } |
| + deadline, ok := canonicalStamp(c.ExecutionDeadline) |
| + if !ok { |
| + return false |
| + } |
| + switch c.Version { |
| + case 1: |
| + return c.ExecutionKind == "" && c.OperationID == "" && c.ExpectedStateSHA256 == "" && c.ClosedCheckpoint == nil |
| + case 2: |
| + if c.ExecutionKind != "compatibility-check" || !wireid.ValidID(c.OperationID) || !wireid.ValidDigest(c.ExpectedStateSHA256) || c.ClosedCheckpoint == nil { |
| + return false |
| + } |
| + p := c.ClosedCheckpoint |
| + completed, ok := canonicalStamp(p.CompletedAt) |
| + if !ok || completed.After(deadline) || p.Version != 1 || !wireid.ValidID(p.PreparationID) || p.MainLength <= 0 || p.MainLength > 268435456 || !wireid.ValidDigest(p.MainSHA256) || !wireid.ValidDigest(p.EvidenceSHA256) { |
| + return false |
| + } |
| + return p.EvidenceSHA256 == c.checkpointDigest() |
| + } |
| + return false |
| +} |
| + |
| +// CheckpointDigest correlates private bindings; it does not certify that a |
| +// checkpoint, writer exclusion or read-only namespace actually exists. |
| +func (c LaunchContext) CheckpointDigest() (string, error) { |
| + if c.Version != 2 || c.ClosedCheckpoint == nil { |
| + return "", ErrLaunch |
| + } |
| + return c.checkpointDigest(), nil |
| +} |
| +func (c LaunchContext) checkpointBytes() []byte { |
| + p := c.ClosedCheckpoint |
| + raw, _ := json.Marshal([]any{"carlos/closed-checkpoint/v1", c.Account, c.App, c.Host, c.OperationID, c.RequestID, c.AttemptID, c.RequestDigest, c.RecordIdentity, c.ArtifactDigest, c.EnvironmentEnvelopeDigest, c.GuardGeneration, c.InputSHA256, c.ExpectedStateSHA256, c.DatabasePath, c.DatabaseDevice, c.DatabaseInode, c.ExecutionDeadline, p.PreparationID, p.CompletedAt, p.MainLength, p.MainSHA256}) |
| + return raw |
| +} |
| +func (c LaunchContext) checkpointDigest() string { |
| + sum := sha256.Sum256(c.checkpointBytes()) |
| + return hex.EncodeToString(sum[:]) |
| +} |
diff --git a/internal/maintenance/launch_test.go b/launchctx/launch_test.go| similarity index 99% |
| rename from internal/maintenance/launch_test.go |
| rename to launchctx/launch_test.go |
| index 51faa7c22..e9d413c19 100644 |
| --- a/internal/maintenance/launch_test.go |
| +++ b/launchctx/launch_test.go |
| @@ -1,4 +1,4 @@ |
| -package maintenance |
| +package launchctx |
| import ( |
| "bytes" |
diff --git a/internal/maintenance/launch_utf8.go b/launchctx/launch_utf8.go| similarity index 97% |
| rename from internal/maintenance/launch_utf8.go |
| rename to launchctx/launch_utf8.go |
| index ba1cdb955..1b9c6c8e5 100644 |
| --- a/internal/maintenance/launch_utf8.go |
| +++ b/launchctx/launch_utf8.go |
| @@ -1,4 +1,4 @@ |
| -package maintenance |
| +package launchctx |
| import "strconv" |
diff --git a/launchctx/mode.go b/launchctx/mode.go| new file mode 100644 |
| index 000000000..caf45da08 |
| --- /dev/null |
| +++ b/launchctx/mode.go |
| @@ -0,0 +1,17 @@ |
| +package launchctx |
| + |
| +func DecodeOperator(raw []byte) (LaunchContext, error) { |
| + c, err := DecodeLaunch(raw) |
| + if err != nil || c.Version != 1 { |
| + return LaunchContext{}, ErrLaunch |
| + } |
| + return c, nil |
| +} |
| + |
| +func DecodeChecker(raw []byte) (LaunchContext, error) { |
| + c, err := DecodeLaunch(raw) |
| + if err != nil || c.Version != 2 { |
| + return LaunchContext{}, ErrLaunch |
| + } |
| + return c, nil |
| +} |
diff --git a/launchctx/mode_test.go b/launchctx/mode_test.go| new file mode 100644 |
| index 000000000..11496f1e7 |
| --- /dev/null |
| +++ b/launchctx/mode_test.go |
| @@ -0,0 +1,31 @@ |
| +package launchctx |
| + |
| +import "testing" |
| + |
| +func TestModeDecodersRefuseOtherLaunchClass(t *testing.T) { |
| + v2, c := launchFixture(t) |
| + if _, err := DecodeChecker(v2); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := DecodeOperator(v2); err == nil { |
| + t.Fatal("checker accepted as mutable operator") |
| + } |
| + c.Version = 1 |
| + c.ExecutionKind, c.OperationID, c.ExpectedStateSHA256 = "", "", "" |
| + c.ClosedCheckpoint = nil |
| + v1, err := EncodeLaunch(c) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := DecodeOperator(v1); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := DecodeChecker(v1); err == nil { |
| + t.Fatal("mutable operator accepted as checker") |
| + } |
| + for _, decode := range []func([]byte) (LaunchContext, error){DecodeOperator, DecodeChecker} { |
| + if _, err := decode([]byte(`{}`)); err == nil { |
| + t.Fatal("malformed context accepted") |
| + } |
| + } |
| +} |
diff --git a/internal/maintenance/testdata/generate-launch-vector.py b/launchctx/testdata/generate-launch-vector.py| similarity index 100% |
| rename from internal/maintenance/testdata/generate-launch-vector.py |
| rename to launchctx/testdata/generate-launch-vector.py |
diff --git a/internal/maintenance/testdata/launch-v2-binding.json b/launchctx/testdata/launch-v2-binding.json| similarity index 100% |
| rename from internal/maintenance/testdata/launch-v2-binding.json |
| rename to launchctx/testdata/launch-v2-binding.json |
diff --git a/internal/maintenance/testdata/launch-v2.json b/launchctx/testdata/launch-v2.json| similarity index 100% |
| rename from internal/maintenance/testdata/launch-v2.json |
| rename to launchctx/testdata/launch-v2.json |
diff --git a/launchctx/wireid/id.go b/launchctx/wireid/id.go| new file mode 100644 |
| index 000000000..dcbe172f6 |
| --- /dev/null |
| +++ b/launchctx/wireid/id.go |
| @@ -0,0 +1,21 @@ |
| +package wireid |
| + |
| +import ( |
| + "encoding/base64" |
| + "encoding/hex" |
| +) |
| + |
| +func ValidID(s string) bool { |
| + if len(s) != 43 { |
| + return false |
| + } |
| + b, e := base64.RawURLEncoding.Strict().DecodeString(s) |
| + return e == nil && len(b) == 32 && base64.RawURLEncoding.EncodeToString(b) == s |
| +} |
| +func ValidDigest(s string) bool { |
| + if len(s) != 64 { |
| + return false |
| + } |
| + b, e := hex.DecodeString(s) |
| + return e == nil && len(b) == 32 && hex.EncodeToString(b) == s |
| +} |