package inventory import ( "context" "crypto/sha256" "encoding/hex" "fmt" "sort" "strings" "github.com/novox/mesh-controller/internal/secrets" ) // A credential a provider's software issued, kept as the provision's one credential (novox/hq ADR 0305, issue // 526). // // **The software made it, so the mesh carries it.** A media server's sign-in token comes from its vendor; the mesh // can neither make nor rotate it. The controller fetches it from the provider's own tool, sealed to a key made for // that one call, and hands the value here, which seals it to every consumer that holds the provision from that // provider — in the pair credential's shape, so a consumer's definition does not know where it came from (ADR 0158 // §5) — and to the operator, and forgets it. // // **Asked often, written seldom.** The provider asks hourly and on every start. The mesh keeps no plaintext, so it // cannot compare values; it stamps every row with the value's fingerprint instead, and a value whose stamp every // row already carries is not sealed again — sealing anew would change every consumer's declaration and restart // each of them for nothing. // OriginIssued is a pair credential the provider's software issued and the provider gave (novox/hq ADR 0305). const OriginIssued = "issued" // IssuedStamp is the generation a value issued by a provider's software is kept under: a fingerprint of the value, // so the same value fetched again is known as the same without being kept. func IssuedStamp(value string) string { sum := sha256.Sum256([]byte(value)) return "issued:sha256:" + hex.EncodeToString(sum[:8]) } // IssuedKept is what keeping an issued value did: whether it was sealed anew, the pairs that hold it, and the // machines a send must reach — the provider's and every holder's — when it was. type IssuedKept struct { Changed bool // Holders are every pair that holds the provision from the provider; Sealed those sealed anew by this act. Holders []Holder Sealed []Holder Machines []string // Unsealable are the holders whose machine has no sealing key: left as they are, said by name. Unsealable []Holder } // KeepIssuedSecret seals a value a provider's software issued to every pair of provision from provider that does // not hold it yet, under one stamp; a pair that does is left as it is, and when every pair does nothing is written // (novox/hq ADR 0305 §4, §5). func (i *Inventory) KeepIssuedSecret(ctx context.Context, provision, provider, value string) (IssuedKept, error) { if strings.TrimSpace(value) == "" { return IssuedKept{}, fmt.Errorf("there is nothing to seal") } providerNode, err := i.NodeByName(ctx, provider) if err != nil { return IssuedKept{}, err } providerKey, err := i.SealingKeyOf(ctx, provider) if err != nil { return IssuedKept{}, err } if providerKey == "" { return IssuedKept{}, fmt.Errorf("%s has no sealing key, so %s cannot be sealed to it", provider, provision) } stamp := IssuedStamp(value) type row struct { holder Holder consumerID any generation, heldC, heldP string consumerKey string } rows, err := i.store.Pool().Query(ctx, `select s.consumer, c.name, s.consumer_module, s.local, coalesce(s.generation, ''), coalesce(s.consumer_key, ''), coalesce(s.provider_key, ''), coalesce(c.sealing_key, '') from secret s join node c on c.id = s.consumer where s.provider = $1 and s.name = $2 order by c.name, s.consumer_module, s.local`, providerNode.ID, provision) if err != nil { return IssuedKept{}, err } var held []row for rows.Next() { var r row if err := rows.Scan(&r.consumerID, &r.holder.Consumer, &r.holder.ConsumerModule, &r.holder.Local, &r.generation, &r.heldC, &r.heldP, &r.consumerKey); err != nil { rows.Close() return IssuedKept{}, err } r.holder.Provision, r.holder.Provider = provision, provider held = append(held, r) } rows.Close() if err := rows.Err(); err != nil { return IssuedKept{}, err } var kept IssuedKept var stale []row for _, r := range held { if r.consumerKey == "" { kept.Unsealable = append(kept.Unsealable, r.holder) continue } kept.Holders = append(kept.Holders, r.holder) // Only a pair that does not hold this value, sealed to the keys its ends have now, is sealed anew: a // holder that already does is left byte for byte as it is, so it is not restarted for another's sake. if r.generation != stamp || r.heldC != r.consumerKey || r.heldP != providerKey { stale = append(stale, r) } } if len(stale) == 0 { return kept, nil } forOperator, operatorKey, err := i.operatorSeal(ctx, value) if err != nil { return IssuedKept{}, err } tx, err := i.store.Pool().Begin(ctx) if err != nil { return IssuedKept{}, err } defer func() { _ = tx.Rollback(ctx) }() machines := map[string]bool{provider: true} for _, r := range stale { sealed, err := secrets.Accept(value, r.consumerKey, providerKey) if err != nil { return IssuedKept{}, err } if _, err := tx.Exec(ctx, `update secret set for_consumer = $6, for_provider = $7, consumer_key = $8, provider_key = $9, operator_sealed = $10, operator_key = $11, origin = $12, generation = $13, created_at = now() where name = $1 and consumer = $2 and consumer_module = $3 and provider = $4 and local = $5`, provision, r.consumerID, r.holder.ConsumerModule, providerNode.ID, r.holder.Local, sealed.ForConsumer, sealed.ForProvider, sealed.ConsumerKey, sealed.ProviderKey, forOperator, operatorKey, OriginIssued, stamp); err != nil { return IssuedKept{}, err } machines[r.holder.Consumer] = true kept.Sealed = append(kept.Sealed, r.holder) } if err := tx.Commit(ctx); err != nil { return IssuedKept{}, err } kept.Changed = true for m := range machines { kept.Machines = append(kept.Machines, m) } sort.Strings(kept.Machines) return kept, nil }