Bring in the adoption handing (fix/1945-adopt-hashes 17478994): the have's user list carries Handed beside Follows and Key; a state that names readers is not Readable to a composer that knows no readers (review of the switch, issue 1945)
This commit is contained in:
@@ -47,7 +47,8 @@ type State struct {
|
||||
Name string `json:"name"`
|
||||
Readable bool `json:"readable,omitempty"`
|
||||
// Readers, when named, are the only modules that may read the state (novox/hq ADR 0547 clause 9, issue 1945): a
|
||||
// state readable by some and not all, such as the vault's bus account hashes. Naming readers makes it readable.
|
||||
// state readable by some and not all, such as the vault's bus account hashes. Only ReadableBy grants them; Readable,
|
||||
// which a composer that knows no readers asks, stays false for it, so such a composer grants nobody.
|
||||
Readers []string `json:"readers,omitempty"`
|
||||
}
|
||||
|
||||
@@ -94,7 +95,7 @@ func (s States) ReadableBy(state, reader string) bool {
|
||||
func (s States) Readable(state string) bool {
|
||||
for _, k := range s.States {
|
||||
if k.Name == state {
|
||||
return k.Readable || len(k.Readers) > 0
|
||||
return k.Readable
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -31,8 +31,8 @@ func TestAnAnnouncementRoundTripsSortedAndSaysWhatIsReadable(t *testing.T) {
|
||||
if !back.Readable("plans-building") || back.Readable("own") || back.Readable("absent") {
|
||||
t.Fatal("Readable does not say what was announced readable")
|
||||
}
|
||||
// Named readers (ADR 0547 clause 9): the state is readable, by them alone.
|
||||
if !back.Readable("hashes") || !back.ReadableBy("hashes", "mesh-grants") || back.ReadableBy("hashes", "spy") ||
|
||||
// Named readers (ADR 0547 clause 9): readable by them alone, and not readable to a composer that knows no readers.
|
||||
if back.Readable("hashes") || !back.ReadableBy("hashes", "mesh-grants") || back.ReadableBy("hashes", "spy") ||
|
||||
!back.ReadableBy("plans-building", "spy") || back.ReadableBy("own", "spy") {
|
||||
t.Fatal("ReadableBy does not say who was named")
|
||||
}
|
||||
|
||||
+10
-3
@@ -34,9 +34,12 @@
|
||||
// **The release 4 layouts** (issues 1356 to 1359; ADR 0453 clauses 4, 6 and 16): the new fingerprint form (fingerprint2.go),
|
||||
// the indexer's manifests by digest and the new fields of its modules and candidates (manifests.go, source.go), the
|
||||
// builder's build keys (builder.go) and the checker's verdicts and kept verdicts (checker.go). A manifests value is the
|
||||
// one exception to the size above: a manifest is up to about 21 KB, whole, byte for byte.
|
||||
// one exception to the size above: a manifest is up to about 21 KB, whole, byte for byte. The bus module's have on the
|
||||
// node holding mesh-broker is the other: its user list's handing carries a sealed box of every loaded user's hash, some
|
||||
// kilobytes (handed.go).
|
||||
//
|
||||
// Standard library only, and this module's own asks and declaration packages (a composition holds declaration resources).
|
||||
// Standard library only, and this module's own asks, declaration and seal packages (a composition holds declaration
|
||||
// resources; the adoption handing is a sealed box, handed.go).
|
||||
package delivery
|
||||
|
||||
import (
|
||||
@@ -358,6 +361,9 @@ type UserListWritten struct {
|
||||
// Key is the bus node's sealing key, its public half as SPKI PEM: the key the grants part seals its list to
|
||||
// (clause 7). Public, as the vault holds it.
|
||||
Key string `json:"key,omitempty"`
|
||||
// Handed is the hashes of the users of the list on the machine, sealed to the vault, for its adoption of the
|
||||
// accounts it holds no value for (ADR 0547 clause 9, handed.go); nil while the vault's key is not known.
|
||||
Handed *AccountsHanded `json:"handed,omitempty"`
|
||||
}
|
||||
|
||||
// HaveNode is a node's own facts, written under HaveNodeKey. Each field is the node-engine's own JSON, carried whole:
|
||||
@@ -508,7 +514,8 @@ func SameHave(a, b Have) bool {
|
||||
}
|
||||
x, y := *ua, *ub
|
||||
x.At, y.At = time.Time{}, time.Time{}
|
||||
return x == y && ua.At.Equal(ub.At)
|
||||
x.Handed, y.Handed = nil, nil
|
||||
return x == y && ua.At.Equal(ub.At) && sameHanded(ua.Handed, ub.Handed)
|
||||
}
|
||||
|
||||
// SameHaveNode says whether two node facts are the same value, comparing each carried JSON without its spacing.
|
||||
|
||||
@@ -170,6 +170,47 @@ func TestRoundTrip(t *testing.T) {
|
||||
if !reflect.DeepEqual(out, in) || !out.UserList.At.Equal(at) {
|
||||
t.Fatalf("got %+v want %+v", out, in)
|
||||
}
|
||||
// And with the hashes handed to the vault (ADR 0547 clause 9): sealed to its key, opened by it alone, every
|
||||
// listed user with a bcrypt hash once, and refused to another key.
|
||||
hash := "$2a$11$" + strings.Repeat("a", 53)
|
||||
list := []byte("{ user: \"controller\", password: \"" + hash + "\", permissions: {\n" +
|
||||
"{ user: \"node.ace\", password: \"" + hash + "\", permissions: {\n" +
|
||||
"{ user: \"controller\", password: \"" + hash + "\" }\n{ user: \"nkey\", password: \"plain\" }\n")
|
||||
accounts := HandedAccountsOf(list)
|
||||
if len(accounts) != 2 || accounts[0].User != "controller" || accounts[1].User != "node.ace" || accounts[1].Hash != hash {
|
||||
t.Fatalf("handed accounts: %+v", accounts)
|
||||
}
|
||||
vault, err := seal.GenerateKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
other, _ := seal.GenerateKeyPair()
|
||||
handed, err := HandAccounts(accounts, vault.PublicKey, "sha256:ab", at)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
in.UserList.Handed = &handed
|
||||
b, err = EncodeHave(in)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(b), "node.ace") || strings.Contains(string(b), hash) {
|
||||
t.Fatalf("a user or a hash is in the clear: %s", b)
|
||||
}
|
||||
out, err = DecodeHave(b)
|
||||
if err != nil || !SameHave(out, in) {
|
||||
t.Fatalf("handed round trip: %v %+v", err, out.UserList)
|
||||
}
|
||||
opened, err := OpenHanded(*out.UserList.Handed, vault.PublicKey, vault.PrivateKey)
|
||||
if err != nil || !reflect.DeepEqual(opened, accounts) {
|
||||
t.Fatalf("opened %+v, %v", opened, err)
|
||||
}
|
||||
if _, err := OpenHanded(*out.UserList.Handed, other.PublicKey, other.PrivateKey); err == nil {
|
||||
t.Fatal("a handing opened for another key")
|
||||
}
|
||||
if _, err := HandAccounts(accounts, "", "sha256:ab", at); err == nil {
|
||||
t.Fatal("handed with no vault key")
|
||||
}
|
||||
})
|
||||
// The grants part's list sealed to the bus node (novox/hq ADR 0547 clause 7): the key's value round-trips and refuses
|
||||
// what the bus node could not follow, and the object opens with the node's key alone, to the digest the key names.
|
||||
@@ -370,6 +411,20 @@ func TestSame(t *testing.T) {
|
||||
if SameHave(ua, ub) {
|
||||
t.Error("different user lists same")
|
||||
}
|
||||
// A handing is compared by what it says: one said and one not differ, as do two to other keys.
|
||||
ub.UserList.Revision = 1
|
||||
ua.UserList.Handed = &AccountsHanded{From: "sha256:a", To: "sha256:k", Users: 2, At: at}
|
||||
if SameHave(ua, ub) {
|
||||
t.Error("a handing said and one not are the same")
|
||||
}
|
||||
ub.UserList.Handed = &AccountsHanded{From: "sha256:a", To: "sha256:k", Users: 2, At: at.In(time.Local)}
|
||||
if !SameHave(ua, ub) {
|
||||
t.Error("equal handings differ")
|
||||
}
|
||||
ub.UserList.Handed.To = "sha256:j"
|
||||
if SameHave(ua, ub) {
|
||||
t.Error("handings to other keys same")
|
||||
}
|
||||
n := HaveNode{V: 1, Busy: json.RawMessage(`true`)}
|
||||
if !SameHaveNode(n, HaveNode{V: 1, Busy: json.RawMessage(`true`)}) || SameHaveNode(n, HaveNode{V: 1}) {
|
||||
t.Error("have node")
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/seal"
|
||||
)
|
||||
|
||||
// The adoption handing of novox/hq ADR 0547 clause 9 (issue 1945): the vault adopts, once and before `take-over`, the
|
||||
// bcrypt hashes of the bus accounts it holds no value for (the controller's own, the node accounts, the enrolled
|
||||
// devices) from the list the bus has loaded. The node-engine of the node holding mesh-broker is the one part that has
|
||||
// that list, so it hands the hashes in the bus module's have, beside what it says of the list it wrote: sealed to the
|
||||
// vault's own key, which it learns from the vault's answer to `node-key`, so that of every reader of the have bucket
|
||||
// the vault alone can open them. The have says in the clear only how many users it hands, from which list and to which
|
||||
// key; the users' names stay sealed, as the list itself is (ADR 0547 clause 7: it names every account).
|
||||
//
|
||||
// It hands every user of the loaded list; the vault takes from it only the users it holds no value for, and never
|
||||
// replaces a hash it made from a value it holds, so the node-engine never needs to read the vault's state (which the
|
||||
// grants part alone is to read, clause 9).
|
||||
|
||||
// AccountsHanded is the hashes of the loaded list's users, sealed to the vault, in UserListWritten.
|
||||
type AccountsHanded struct {
|
||||
// From is the digest of the list on the machine the hashes are taken from, `sha256:<hex>` (UserListWritten.Digest).
|
||||
From string `json:"from"`
|
||||
// To is the fingerprint of the vault's key the box is sealed to (KeyFingerprint).
|
||||
To string `json:"to"`
|
||||
// Users is how many users the box holds.
|
||||
Users int `json:"users"`
|
||||
// Box is the JSON of []HandedAccount, sealed to the vault's key (ADR 0206).
|
||||
Box seal.Box `json:"box"`
|
||||
At time.Time `json:"at"`
|
||||
}
|
||||
|
||||
// HandedAccount is one user of an AccountsHanded box, opened: the bus user and the bcrypt hash the loaded list
|
||||
// carries for it.
|
||||
type HandedAccount struct {
|
||||
User string `json:"user"`
|
||||
Hash string `json:"hash"`
|
||||
}
|
||||
|
||||
// KeyFingerprint is a public key's fingerprint as a handing names it: `sha256:<hex>` of its PEM, without the space
|
||||
// around it.
|
||||
func KeyFingerprint(publicPEM string) string {
|
||||
sum := sha256.Sum256([]byte(strings.TrimSpace(publicPEM)))
|
||||
return "sha256:" + hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// listedAccount is one user of a bus user list as the composer writes it (buscompose), with its password's hash.
|
||||
var listedAccount = regexp.MustCompile(`\{\s*user:\s*"([^"]+)",\s*password:\s*"([^"]+)"`)
|
||||
|
||||
// HandedAccountsOf is every user of a bus user list that carries a bcrypt hash, in the list's order. A user listed
|
||||
// twice is handed once, as the list first names it.
|
||||
func HandedAccountsOf(list []byte) []HandedAccount {
|
||||
var out []HandedAccount
|
||||
seen := map[string]bool{}
|
||||
for _, m := range listedAccount.FindAllSubmatch(list, -1) {
|
||||
user, hash := string(m[1]), string(m[2])
|
||||
if seen[user] || !IsBcrypt(hash) {
|
||||
continue
|
||||
}
|
||||
seen[user] = true
|
||||
out = append(out, HandedAccount{User: user, Hash: hash})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// bcryptHash is the form of a bcrypt hash: version, cost, and 53 characters of salt and digest.
|
||||
var bcryptHash = regexp.MustCompile(`^\$2[abxy]?\$[0-9]{2}\$[./A-Za-z0-9]{53}$`)
|
||||
|
||||
// IsBcrypt says whether s has the form of a bcrypt hash.
|
||||
func IsBcrypt(s string) bool { return bcryptHash.MatchString(s) }
|
||||
|
||||
// HandAccounts seals the accounts to the vault's public key (SPKI PEM) as the handing of the list whose digest is from.
|
||||
func HandAccounts(accounts []HandedAccount, vaultPEM, from string, at time.Time) (AccountsHanded, error) {
|
||||
if strings.TrimSpace(vaultPEM) == "" {
|
||||
return AccountsHanded{}, errors.New("delivery: no vault key to seal the handed accounts to")
|
||||
}
|
||||
body, err := json.Marshal(accounts)
|
||||
if err != nil {
|
||||
return AccountsHanded{}, err
|
||||
}
|
||||
box, err := seal.Seal(string(body), vaultPEM)
|
||||
if err != nil {
|
||||
return AccountsHanded{}, fmt.Errorf("delivery: the handed accounts could not be sealed to the vault's key: %w", err)
|
||||
}
|
||||
return AccountsHanded{From: from, To: KeyFingerprint(vaultPEM), Users: len(accounts), Box: box, At: at.UTC()}, nil
|
||||
}
|
||||
|
||||
// OpenHanded opens a handing with the vault's private key (PEM), refusing one sealed to another key than publicPEM's
|
||||
// and a box that does not hold as many users as it says.
|
||||
func OpenHanded(h AccountsHanded, publicPEM, privatePEM string) ([]HandedAccount, error) {
|
||||
if h.To != KeyFingerprint(publicPEM) {
|
||||
return nil, fmt.Errorf("delivery: the handing is sealed to the key %s, not to this one (%s)", h.To,
|
||||
KeyFingerprint(publicPEM))
|
||||
}
|
||||
plain, err := seal.Open(h.Box, privatePEM)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("delivery: the handing does not open: %w", err)
|
||||
}
|
||||
var out []HandedAccount
|
||||
if err := json.Unmarshal([]byte(plain), &out); err != nil {
|
||||
return nil, fmt.Errorf("delivery: the handing holds no accounts: %w", err)
|
||||
}
|
||||
if len(out) != h.Users {
|
||||
return nil, fmt.Errorf("delivery: the handing says %d users and holds %d", h.Users, len(out))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// sameHanded says whether two handings say the same: the same list, key, count and box, whatever zone their time is in.
|
||||
func sameHanded(a, b *AccountsHanded) bool {
|
||||
if a == nil || b == nil {
|
||||
return a == b
|
||||
}
|
||||
x, y := *a, *b
|
||||
x.At, y.At = time.Time{}, time.Time{}
|
||||
return x == y && a.At.Equal(b.At)
|
||||
}
|
||||
Reference in New Issue
Block a user