package hostactors import ( "crypto/sha256" "encoding/hex" "encoding/json" ) // BoxBinding is the digest installed claim state on one box binds to: the // deployment's binding facts and that box's row identity and state, read from // one verified roster. It changes only when one of those facts changes. Holds, // activations, enrollment attempts, consoles, regions, other boxes' rows, this // box's role, label, group, loss ids and enrollment time, and the roster's // write counter (Revision) never change it, so the console's per-operation // roster writes no longer strand installed instances. // // The second result is false when ref names no row. func BoxBinding(r Roster, ref string) (string, bool) { b, ok := r.Boxes[ref] if !ok { return "", false } sum := sha256.Sum256(boxBindingV1(r, b)) return hex.EncodeToString(sum[:]), true } // ValidBinding reports whether s has BoxBinding's form: 64 lower-case hex // digits. It says nothing about which roster or box it was computed from. func ValidBinding(s string) bool { if len(s) != 2*sha256.Size { return false } for i := 0; i < len(s); i++ { if c := s[i]; (c < '0' || c > '9') && (c < 'a' || c > 'f') { return false } } return true } // boxBindingV1 freezes the binding facts in an ordered v1 tuple, encoded like // the journal's request tuple (hostbox/journalservice.go). Never extend this // tuple when Roster or Box grows: a new bound fact requires a new version tag // and a separate encoder, and every installed stamp changes with it. func boxBindingV1(r Roster, b Box) []byte { raw, _ := json.Marshal([]any{ "carlos-box-binding-v1", r.DeploymentBucket, r.HomeRegion, r.RequestLimitNS, r.Publication, r.FloorSHA256, b.Enrollment, b.Namespace, b.BoxID, b.Region, b.Pool, []any{b.Cloud.Provider, b.Cloud.Account, b.Cloud.Region, b.Cloud.InstanceID}, b.EvidenceSHA256, b.State, b.QuarantineRevision, hex.EncodeToString(b.CensusPublicKey), }) return raw }