Files
mesh-controller/internal/inventory/issued.go
T
jschoubben db86dd52c8
mesh/delivery-group group feat/526-a-provider-delivers-what-its-software-issued rejected: a member's own check failed
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without …
mesh/repo-check pass: its merge-check.sh passed
Take a provider's software-issued credential from the provider, sealed (hq issue 526, ADR 0305)
A token a media server got from its vendor reached its consumers only as a value the mesh minted, which
the server refused, or as a copy typed by hand. An offer may now say its software issues the credential;
the new issued verb carries no value and asks the provider's tool, in the controller's own name, for the
value sealed to one call, then seals it to every pair that does not hold it yet.
2026-10-12 01:07:16 +02:00

156 lines
5.8 KiB
Go

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
}