132 lines
5.0 KiB
Go
132 lines
5.0 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/subtle"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/nacl/box"
|
|
|
|
"github.com/novox/mesh-host/internal/identity"
|
|
"github.com/novox/mesh-host/internal/link"
|
|
)
|
|
|
|
// The move of a value sealed to this machine into the vault (design 57's V3, issue 676; internal/link
|
|
// movesecret.go). The controller cannot open what it holds; this machine can open what was sealed to it, and so
|
|
// hands it to the vault, sealed to a key the vault made for the call. The value is in this process for that one
|
|
// call and written nowhere.
|
|
|
|
// vaultAsk asks one of the vault's verbs in this engine's own name and answers its result.
|
|
type vaultAsk func(verb string, args any) (json.RawMessage, error)
|
|
|
|
// moveSecretFrom is the engine's move: this machine's sealing key, read for each ask, and the vault asked over
|
|
// the link open now.
|
|
func moveSecretFrom(statePath string, queue *link.Session) link.MoveSecretFunc {
|
|
return func(m link.MoveSecret) (link.MovedSecret, error) {
|
|
key, err := identity.LoadSealingKey(identity.SealingKeyPath(statePath))
|
|
if err != nil {
|
|
return link.MovedSecret{}, err
|
|
}
|
|
ask := func(verb string, args any) (json.RawMessage, error) {
|
|
return queue.AskVault(context.Background(), verb, args)
|
|
}
|
|
moved, err := moveIntoTheVault(ask, key, m)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "mesh-host: a value was not moved into the vault: %v\n", err)
|
|
}
|
|
return moved, err
|
|
}
|
|
}
|
|
|
|
// moveIntoTheVault opens the row's value with this machine's sealing key, hands it to the vault's move-in in its two
|
|
// steps, and answers what the vault kept once its read-back — sealed to a key made here for this one call — opens
|
|
// to the same value.
|
|
func moveIntoTheVault(ask vaultAsk, key identity.SealingKey, m link.MoveSecret) (link.MovedSecret, error) {
|
|
if m.SealedTo != "" && m.SealedTo != key.Public {
|
|
return link.MovedSecret{}, errors.New("the controller's store says this value was sealed to a sealing key this " +
|
|
"machine no longer has, so it cannot be opened here")
|
|
}
|
|
value, err := key.Unseal(m.Sealed)
|
|
if err != nil {
|
|
return link.MovedSecret{}, fmt.Errorf("the value does not open with this machine's sealing key: %w", err)
|
|
}
|
|
defer wipe(value)
|
|
|
|
// This machine's sealing key, stated in its own name: the key the vault seals the core's credentials to for the
|
|
// controller to carry here (novox/hq ADR 0349 §16, issue 763), never a key the controller names.
|
|
first := map[string]any{"origin": m.Origin, "generation": m.Generation, "sealing_key": key.Public}
|
|
if !m.MadeAt.IsZero() {
|
|
first["made_at"] = m.MadeAt.UTC().Format(time.RFC3339Nano)
|
|
}
|
|
if m.Own() {
|
|
first["node"], first["module"], first["name"] = m.Node, m.Module, m.Name
|
|
} else {
|
|
first["provider"], first["provision"] = m.Provider, m.Provision
|
|
first["consumer"], first["consumer_module"] = m.Consumer, m.ConsumerModule
|
|
if m.Local != "" {
|
|
first["local"] = m.Local
|
|
}
|
|
}
|
|
raw, err := ask("move-in", first)
|
|
if err != nil {
|
|
return link.MovedSecret{}, fmt.Errorf("the vault did not open the move: %w", err)
|
|
}
|
|
var opened struct {
|
|
Call string `json:"call"`
|
|
SealTo string `json:"seal_to"`
|
|
Secret string `json:"secret"`
|
|
}
|
|
if err := json.Unmarshal(raw, &opened); err != nil || opened.Call == "" || opened.SealTo == "" {
|
|
return link.MovedSecret{}, errors.New("the vault answered no call and no key to seal to")
|
|
}
|
|
sealed, err := identity.Seal(opened.SealTo, value)
|
|
if err != nil {
|
|
return link.MovedSecret{}, fmt.Errorf("the vault's key for the call is not a sealing key: %w", err)
|
|
}
|
|
backPub, backPriv, err := box.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
return link.MovedSecret{}, err
|
|
}
|
|
defer wipe(backPriv[:])
|
|
raw, err = ask("move-in", map[string]any{"call": opened.Call, "sealed": sealed,
|
|
"read_back_to": base64.StdEncoding.EncodeToString(backPub[:])})
|
|
if err != nil {
|
|
return link.MovedSecret{}, fmt.Errorf("the vault did not take %s: %w", opened.Secret, err)
|
|
}
|
|
var kept struct {
|
|
Name string `json:"name"`
|
|
Generation int `json:"generation"`
|
|
Unchanged bool `json:"unchanged"`
|
|
ReadBack string `json:"read_back"`
|
|
}
|
|
if err := json.Unmarshal(raw, &kept); err != nil || kept.Name == "" || kept.ReadBack == "" {
|
|
return link.MovedSecret{}, fmt.Errorf("the vault answered %s with no read-back", opened.Secret)
|
|
}
|
|
blob, err := base64.StdEncoding.DecodeString(kept.ReadBack)
|
|
if err != nil {
|
|
return link.MovedSecret{}, fmt.Errorf("the vault's read-back of %s is not sealed", kept.Name)
|
|
}
|
|
back, ok := box.OpenAnonymous(nil, blob, backPub, backPriv)
|
|
if !ok {
|
|
return link.MovedSecret{}, fmt.Errorf("the vault's read-back of %s does not open with the key made for it", kept.Name)
|
|
}
|
|
defer wipe(back)
|
|
if subtle.ConstantTimeCompare(back, value) != 1 {
|
|
return link.MovedSecret{}, fmt.Errorf("the vault keeps %s, but it read back another value than the one handed "+
|
|
"in: the move of it did not hold", kept.Name)
|
|
}
|
|
return link.MovedSecret{Secret: kept.Name, Generation: kept.Generation, Unchanged: kept.Unchanged}, nil
|
|
}
|
|
|
|
func wipe(b []byte) {
|
|
for i := range b {
|
|
b[i] = 0
|
|
}
|
|
}
|