package delivery import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "strings" "time" "git.novox.be/novox/mesh-sdk/go/seal" ) // **The grants part's user list, sealed to the bus node** (novox/hq ADR 0547 clauses 7 and 8, issue 1945): the contract // between the grants part (mesh-delivery modules/mesh-grants), which writes it, and the node-engine of the node holding // mesh-broker (mesh-host), which follows it once the operator's take-over says so. // // - The grants part composes the bus's user list (the accounts block, buscompose.ComposeAccounts, with the vault's // hashes), seals it to the bus node's sealing key (seal.Seal, the key the node says in its have of the bus module, // UserListWritten.Key), and puts the box as one object of its object bucket GrantsUserListObjects, named by the // list's digest (UserListObjectName). // - It writes the key GrantsUserListKey of its state `user-list` (bucket GrantsUserListBucket): the GrantsUserList // below, which names the object, the list's digest, its revision, its user count and the users left out, and which // source the bus node follows (Follow: FollowGrants from take-over, FollowController in shadow and after hand-back). // - The node-engine of the bus node watches that key alone. While it says FollowGrants it opens the object, checks // the digest, writes the bus's user-list file (mode 0600) only when the file's digest differs, and says what it // loaded in its have of the bus module (UserListWritten, Source GrantsUserListSource); otherwise it follows the // controller's compositions as before (clause 8). Never both. // The names of the grants part's user list on the bus. const ( // GrantsUserListBucket is the grants part's state `user-list`. GrantsUserListBucket = "mesh-grants_user-list" // GrantsUserListKey is the one key of it the bus node reads. GrantsUserListKey = "sealed" // GrantsUserListObjects is the grants part's object bucket, where each sealed list is one object. GrantsUserListObjects = "mesh-grants-user-list" // GrantsUserListObjectStream is the bucket's stream. GrantsUserListObjectStream = "OBJ_" + GrantsUserListObjects // GrantsUserListSource is the source a have names for a list taken from the grants part. GrantsUserListSource = GrantsUserListBucket + "." + GrantsUserListKey ) // Which list the bus node follows. const ( FollowController = "controller" FollowGrants = "grants" ) // GrantsUserList is the value of the grants part's key `user-list.sealed`. type GrantsUserList struct { V int `json:"v"` // Follow is which list the bus node follows: FollowGrants from take-over, FollowController otherwise. Follow string `json:"follow"` // Digest is the list's digest, `sha256:` of the file's bytes as the bus loads them. Digest string `json:"digest"` // Object is the object's name in GrantsUserListObjects (UserListObjectName of Digest). Object string `json:"object"` // SealedTo is the fingerprint of the key the object is sealed to (SealingKeyID), and Node the node it holds. SealedTo string `json:"sealed_to"` Node string `json:"node"` // Revision counts the lists the grants part wrote: one more for each new digest. Revision uint64 `json:"revision"` Users int `json:"users"` // LeftOut are the users the list leaves out for want of a hash in the vault. LeftOut []string `json:"left_out"` At time.Time `json:"at"` } // EncodeGrantsUserList is g as JSON at Version; it refuses a value the bus node could not follow. func EncodeGrantsUserList(g GrantsUserList) ([]byte, error) { g.V = Version if g.LeftOut == nil { g.LeftOut = []string{} } if err := g.check(); err != nil { return nil, err } return json.Marshal(g) } // DecodeGrantsUserList reads the key's value; it refuses another version and what Encode refuses. func DecodeGrantsUserList(data []byte) (GrantsUserList, error) { var g GrantsUserList if err := decode(data, &g); err != nil { return GrantsUserList{}, err } if err := g.check(); err != nil { return GrantsUserList{}, err } return g, nil } func (g GrantsUserList) check() error { if g.Follow != FollowGrants && g.Follow != FollowController { return fmt.Errorf("delivery: the user list's follow %q is not %s or %s", g.Follow, FollowGrants, FollowController) } if g.Digest == "" { return nil // a switch with no list written yet: the bus node keeps what it follows } if !strings.HasPrefix(g.Digest, "sha256:") || g.Object != UserListObjectName(g.Digest) { return fmt.Errorf("delivery: the user list names object %q for digest %q; the object is the digest's hex", g.Object, g.Digest) } return nil } // UserListDigest is a list's digest as the have and the key say it, `sha256:` of its bytes. func UserListDigest(content []byte) string { sum := sha256.Sum256(content) return "sha256:" + hex.EncodeToString(sum[:]) } // UserListObjectName is the object a list of that digest is put under: the digest's hex. func UserListObjectName(digest string) string { return strings.TrimPrefix(digest, "sha256:") } // SealingKeyID is a short fingerprint of a public key in PEM, `sha256:<16 hex>`, to say which key a box is sealed to // without carrying the key. func SealingKeyID(publicPEM string) string { sum := sha256.Sum256([]byte(strings.TrimSpace(publicPEM))) return "sha256:" + hex.EncodeToString(sum[:8]) } // SealUserList is the object a list is put as: the sealed box (seal.Seal) of its bytes to the bus node's key, as JSON. func SealUserList(list []byte, nodePEM string) ([]byte, error) { if !strings.Contains(nodePEM, "PUBLIC KEY") { return nil, fmt.Errorf("delivery: the bus node's key is not a public key in PEM") } box, err := seal.Seal(string(list), nodePEM) if err != nil { return nil, fmt.Errorf("delivery: the user list could not be sealed: %w", err) } return json.Marshal(box) } // OpenUserList opens an object with open (the node's own key) and checks it is the list of digest. func OpenUserList(object []byte, digest string, open func(seal.Box) ([]byte, error)) ([]byte, error) { var box seal.Box if err := json.Unmarshal(object, &box); err != nil { return nil, fmt.Errorf("delivery: the object is no sealed box: %w", err) } list, err := open(box) if err != nil { return nil, err } if got := UserListDigest(list); got != digest { return nil, fmt.Errorf("delivery: the opened list's digest is %s, and the key names %s", got, digest) } return list, nil }