Files
mesh-catalog/modules/mesh-vault/cmd/mesh-vault/enrolment.go
T

581 lines
25 KiB
Go

package main
// The vault's part of enrolment (novox/hq ADR 0587 §3, §5, §8; ADR 0561 §4, §5; issue 774).
//
// A machine joins with a token a person types. The seat mesh-node-enroller decides who may join; the vault holds the secrets.
// Its verb `enrolment-token` makes the token's one-time secret, keeps it with its operator copy as `enrol/<node>/token`,
// writes its bcrypt hash under `bus-accounts` as the user `enrol.<node>`, and answers at issue the public fields alone
// (never the token). Asked to show it, it answers the whole token sealed to the operator key, which the operator opens at
// their own terminal (`nox enrol open`); asked to drop it, it takes the token and its hash away. Its verb `node-account`
// makes the account `node.<node>` of a joining machine at the redeem: once for a node and a set of keys, sealed to the key
// the machine presented, with the node's key in `node-logins` naming that login in use and nothing rotating. Only the
// seat mesh-node-enroller may call either: the runtime proves who asks.
import (
"crypto/ecdh"
"crypto/ed25519"
"crypto/rand"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
enrollerclient "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient"
"os"
"path/filepath"
"strings"
"time"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
stdio "git.novox.be/novox/mesh-sdk/go"
"golang.org/x/crypto/bcrypt"
)
// The names enrolment gives: the module that holds the seat mesh-node-enroller, the prefix of a token's bus user, and the
// vault's state of each node's logins.
const (
EnrolmentModule = "mesh-enrolment"
EnrolUserPrefix = enrollerclient.UserPrefix
NodeLoginsState = meshvault.StateNodeLogins
// MeshHostModule is the module a node's own login secret is kept under (`own/<node>/mesh-host/<login>`, ADR 0583 §3).
MeshHostModule = "mesh-host"
// vaultModule is this module: the holder of the seat mesh-vault, whose states the reads and the bus names carry.
vaultModule = "mesh-vault"
)
// Events the vault says for enrolment's accounts are account-hashed (accounts.go), never carrying a hash or a value.
// enrolNode says node is a name a joining machine can have: one the mesh writes, with no dot, and not shaped like a
// second login.
func enrolNode(node string) error {
if !namePart.MatchString(node) || strings.Contains(node, ".") || strings.Contains(node, SecondLoginSuffix) {
return fmt.Errorf("%q is not a name a node can have: lower-case words, no dot, and not ending in %s", node, SecondLoginSuffix)
}
return nil
}
// enrolSecret is the vault's name of a node's enrolment token.
func enrolSecret(node string) string { return "enrol/" + node + "/token" }
// enrolSecretName says name is an enrolment token, which is handed to nobody: only shown, sealed to the operator key.
func enrolSecretName(name string) bool { return strings.HasPrefix(name, "enrol/") }
// loginSecret is the vault's name of a node's login, the value its node-engine takes (ADR 0583 §3 insight).
func loginSecret(node, login string) string {
return "own/" + node + "/" + MeshHostModule + "/" + login
}
// nodeLoginSecret says name is a node's login secret, and for which node and login. A node's login is handed to that
// node's node-engine alone.
func nodeLoginSecret(name string) (node, login string, ok bool) {
parts := strings.Split(name, "/")
if len(parts) != 4 || parts[0] != "own" || parts[2] != MeshHostModule {
return "", "", false
}
base := strings.TrimSuffix(parts[3], SecondLoginSuffix)
if base != NodeEnginePrefix+parts[1] {
return "", "", false
}
return parts[1], parts[3], true
}
// loginRefusal says why caller may not have the secret name when it is a node's login, or "": a node's login is the
// node-engine of that node's alone (`node.<node>` or its second login), sealed to the key that machine stated.
func (v *Vault) loginRefusal(caller, name string) (isLogin bool, why string) {
node, _, ok := nodeLoginSecret(name)
if !ok {
return false, ""
}
engine, _, _ := strings.Cut(strings.TrimPrefix(caller, NodeEnginePrefix), SecondLoginSuffix)
if !strings.HasPrefix(caller, NodeEnginePrefix) || engine != node {
return true, fmt.Sprintf("%s is a login of %s's node-engine and is handed to that machine's engine alone (%s or its second login)",
name, node, NodeEnginePrefix+node)
}
return true, ""
}
// loginKeyRefusal says why a node's login is not sealed to the key asked: it is sealed to the key the node's node-engine
// stated (node-key) and to no other, or "".
func (v *Vault) loginKeyRefusal(caller, publicKey string) string {
node, _ := loginNode(caller)
stated, had, err := v.Store.NodeKey(node)
if err != nil {
return "the key " + node + "'s node-engine stated could not be read: " + err.Error()
}
if !had {
return "the vault holds no sealing key that " + node + "'s node-engine stated (node-key)"
}
want, err := publicPEM(stated)
if err != nil {
return "the key " + node + "'s node-engine stated is not usable: " + err.Error()
}
given, err := askerKey(publicKey)
if err != nil || strings.TrimSpace(given) != strings.TrimSpace(want) {
return "a node's login is sealed to the key its node-engine stated, and the key sent is another"
}
return ""
}
// fromEnrolment refuses every caller but the seat mesh-node-enroller, as the runtime proved it.
func (v *Vault) fromEnrolment(caller, verb string) error {
callers := v.EnrolCallers
if callers == nil {
callers = stdio.Callers{EnrolmentModule}
}
if !callers.Allows(caller) {
if caller == "" {
caller = stdio.Unproven
}
return fmt.Errorf("%s is asked by the seat %s alone, which decides who may join; %s is not it (novox/hq ADR 0587 §3)",
verb, EnrolmentModule, caller)
}
return nil
}
// TokenBase is what a token states of the bus beside its secret: where the bus is and the certificate to expect there.
// The machine's first tunnel comes with the issue call (ADR 0587 §3, insight 2026-11-02).
type TokenBase struct {
Broker string `json:"broker"`
Fingerprint string `json:"fingerprint"`
}
// EnrolmentTokenArgs are `enrolment-token`'s arguments (the seat's contract, mesh-sdk delivery): the node, when it expires
// (the seat's word), the machine's first tunnel at issue, and which of the three acts: issue (neither switch), sealed or
// drop.
type EnrolmentTokenArgs struct {
Node string
Expires string
Tunnel *enrollerclient.TokenTunnel
Sealed bool
Drop bool
}
// EnrolmentTokenAnswer is mesh-sdk's meshvault.EnrolmentTokenAnswer, the wire shape mesh-enrolment decodes: at issue the
// bus's address, its certificate's fingerprint and the vault's public signing key; when sealed, the whole token sealed to
// the operator key; at drop, whether it was dropped. Never a token in the clear.
type EnrolmentTokenAnswer = meshvault.EnrolmentTokenAnswer
// enrolTunnel is the vault's name of the first tunnel a node's token carries, kept beside the token.
func enrolTunnel(node string) string { return "enrol/" + node + "/tunnel" }
// EnrolmentToken answers caller's `enrolment-token`.
func (v *Vault) EnrolmentToken(caller string, a EnrolmentTokenArgs) (EnrolmentTokenAnswer, error) {
if err := v.fromEnrolment(caller, "enrolment-token"); err != nil {
return EnrolmentTokenAnswer{}, err
}
if err := enrolNode(a.Node); err != nil {
return EnrolmentTokenAnswer{}, err
}
if v.Accounts == nil {
return EnrolmentTokenAnswer{}, errors.New("the vault has no state of bus accounts to write to")
}
switch {
case a.Drop && a.Sealed:
return EnrolmentTokenAnswer{}, errors.New("a token is answered sealed or dropped, not both")
case a.Drop:
return v.dropEnrolmentToken(caller, a.Node)
case a.Sealed:
return v.showEnrolmentToken(caller, a.Node)
}
return v.issueEnrolmentToken(caller, a)
}
// issueEnrolmentToken makes the secret afresh (an earlier token of the node is replaced), keeps it with its operator copy,
// and writes its hash as the user `enrol.<node>`. Its answer carries the public fields alone.
func (v *Vault) issueEnrolmentToken(caller string, a EnrolmentTokenArgs) (EnrolmentTokenAnswer, error) {
// What the machine must trust is known before anything is kept: the bus, its certificate, the vault's signing key,
// and the first tunnel whole (ADR 0587 §3), so the token shown later is one a machine can use.
base, err := v.tokenBase()
if err != nil {
return EnrolmentTokenAnswer{}, err
}
keys, err := v.keys()
if err != nil {
return EnrolmentTokenAnswer{}, fmt.Errorf("the token carries the vault's signing key, which is not held yet: %w", err)
}
signer := keys.key.Public().(ed25519.PublicKey)
if a.Tunnel == nil {
return EnrolmentTokenAnswer{}, errors.New("enrolment-token at issue carries the machine's first tunnel (`tunnel`: key, address, range, " +
"hub_key, hub_endpoint), so the machine reaches the bus over the private network (novox/hq ADR 0587 §3)")
}
if missing := (enrollerclient.Token{Version: 1, Broker: base.Broker, Fingerprint: base.Fingerprint, Signer: signer,
Secret: "-", Tunnel: a.Tunnel}).Missing(); len(missing) > 0 {
return EnrolmentTokenAnswer{}, fmt.Errorf("the token of %s would lack %s: nothing was issued", a.Node, strings.Join(missing, ", "))
}
tunnel, err := json.Marshal(a.Tunnel)
if err != nil {
return EnrolmentTokenAnswer{}, err
}
raw := make([]byte, 30)
if _, err := rand.Read(raw); err != nil {
return EnrolmentTokenAnswer{}, err
}
secret := base64.RawURLEncoding.EncodeToString(raw)
name, user := enrolSecret(a.Node), EnrolUserPrefix+a.Node
c, err := v.keepWith(name, secret, "the vault", caller, OriginMade, keepOptions{quiet: true})
if err != nil {
return EnrolmentTokenAnswer{}, fmt.Errorf("the token of %s is not kept: %w", a.Node, err)
}
hash, err := bcrypt.GenerateFromPassword([]byte(secret), v.hashCost())
if err != nil {
return EnrolmentTokenAnswer{}, err
}
if _, err := v.keepWith(enrolTunnel(a.Node), string(tunnel), "the vault", caller, OriginMade, keepOptions{quiet: true}); err != nil {
return EnrolmentTokenAnswer{}, fmt.Errorf("the first tunnel of %s is not kept: %w", a.Node, err)
}
at := v.Now().UTC()
fp := Fingerprint(secret)
if err := v.Accounts.Put(AccountHash{User: user, Hash: string(hash), Of: name, Generation: c.Generation, Fingerprint: fp,
At: at, Kept: KeptEnrolment}); err != nil {
return EnrolmentTokenAnswer{}, fmt.Errorf("the hash of %s could not be written to %s: %w", user, BusAccountsState, err)
}
v.saidHashed(user, name, c.Generation, fp, at)
_ = v.recordHanding(Handing{To: caller, Name: name, Generation: c.Generation, Outcome: "issued", Why: "enrolment token of " + a.Node})
v.log("made the enrolment token of %s for %s (generation %d); its hash is the user %s", a.Node, caller, c.Generation, user)
return EnrolmentTokenAnswer{Node: a.Node, Broker: base.Broker, Fingerprint: base.Fingerprint,
Signer: base64.StdEncoding.EncodeToString(signer)}, nil
}
// showEnrolmentToken answers the whole token sealed to the operator key: the secret held, the bus's address and
// certificate fingerprint, and the vault's public signing key.
func (v *Vault) showEnrolmentToken(caller, node string) (EnrolmentTokenAnswer, error) {
rec, held, err := v.Store.Read(enrolSecret(node))
if err != nil {
return EnrolmentTokenAnswer{}, err
}
if !held {
return EnrolmentTokenAnswer{}, fmt.Errorf("the vault holds no enrolment token of %s", node)
}
secret, err := openAtRest(rec, v)
if err != nil {
return EnrolmentTokenAnswer{}, fmt.Errorf("the token of %s does not open with the vault's key: %w", node, err)
}
operator, err := v.Store.OperatorKey()
if err != nil {
return EnrolmentTokenAnswer{}, err
}
if operator == "" {
return EnrolmentTokenAnswer{}, errors.New("the vault has no operator key to seal the token to (ADR 0113 §22)")
}
base, err := v.tokenBase()
if err != nil {
return EnrolmentTokenAnswer{}, err
}
keys, err := v.keys()
if err != nil {
return EnrolmentTokenAnswer{}, fmt.Errorf("the token carries the vault's signing key, which is not held yet: %w", err)
}
var tunnel *enrollerclient.TokenTunnel
if trec, theld, err := v.Store.Read(enrolTunnel(node)); err != nil {
return EnrolmentTokenAnswer{}, err
} else if theld {
raw, err := openAtRest(trec, v)
if err != nil {
return EnrolmentTokenAnswer{}, fmt.Errorf("the first tunnel of %s does not open with the vault's key: %w", node, err)
}
tunnel = &enrollerclient.TokenTunnel{}
if err := json.Unmarshal([]byte(raw), tunnel); err != nil {
return EnrolmentTokenAnswer{}, fmt.Errorf("the first tunnel of %s is not readable: %w", node, err)
}
}
signer := keys.key.Public().(ed25519.PublicKey)
tok := enrollerclient.Token{Version: 1, Node: node, Broker: base.Broker, Fingerprint: base.Fingerprint,
Signer: []byte(signer), Secret: secret, Tunnel: tunnel}
if tunnel == nil {
return EnrolmentTokenAnswer{}, fmt.Errorf("the vault keeps no first tunnel for %s's token: issue it again with its tunnel", node)
}
text, err := tok.Encode()
if err != nil {
return EnrolmentTokenAnswer{}, err
}
sealed, err := operatorSeal(operator, []byte(text))
if err != nil {
return EnrolmentTokenAnswer{}, err
}
_ = v.recordHanding(Handing{To: caller, Name: enrolSecret(node), Generation: rec.Generation, Outcome: "shown",
Why: "sealed to the operator key"})
return EnrolmentTokenAnswer{Node: node, Broker: base.Broker, Fingerprint: base.Fingerprint,
Signer: base64.StdEncoding.EncodeToString(signer), Sealed: sealed}, nil
}
// tokenBase is what a token states of the bus, or why the vault knows none.
func (v *Vault) tokenBase() (TokenBase, error) {
if v.TokenBase == nil {
return TokenBase{}, errors.New("the vault does not know the bus's address and certificate fingerprint, so it makes no token to show")
}
return v.TokenBase()
}
// dropEnrolmentToken takes the token and its hash away, at every end of an enrolment (ADR 0587 §5). Nothing held is
// answered as dropped nothing.
func (v *Vault) dropEnrolmentToken(caller, node string) (EnrolmentTokenAnswer, error) {
name, user := enrolSecret(node), EnrolUserPrefix+node
out := EnrolmentTokenAnswer{Node: node}
a, err := v.Accounts.Get(user)
if err != nil {
return out, err
}
if a != nil {
if a.Kept != KeptEnrolment {
return out, fmt.Errorf("%s is no enrolment token's user: nothing was dropped", user)
}
if err := v.Accounts.Delete(user); err != nil {
return out, fmt.Errorf("the hash of %s could not be taken out of %s: %w", user, BusAccountsState, err)
}
out.Dropped = true
}
if _, held, err := v.Store.Read(name); err != nil {
return out, err
} else if held {
if err := v.Store.Delete(name); err != nil {
return out, fmt.Errorf("the token of %s could not be deleted: %w", node, err)
}
out.Dropped = true
}
if _, held, err := v.Store.Read(enrolTunnel(node)); err != nil {
return out, err
} else if held {
if err := v.Store.Delete(enrolTunnel(node)); err != nil {
return out, fmt.Errorf("the first tunnel of %s could not be deleted: %w", node, err)
}
}
if out.Dropped {
_ = v.recordHanding(Handing{To: caller, Name: name, Outcome: "dropped", Why: "enrolment token of " + node})
v.log("dropped the enrolment token of %s and its hash, asked by %s", node, caller)
}
return out, nil
}
// saidHashed says account-hashed for a user whose key the vault wrote: never the hash or the value.
func (v *Vault) saidHashed(user, of string, generation int, fingerprint string, at time.Time) {
if v.Emit == nil {
return
}
if err := v.Emit(AccountHashedEvent, map[string]any{"user": user, "of": of, "generation": generation,
"fingerprint": fingerprint, "at": at.Format(time.RFC3339Nano)}); err != nil {
v.log("the hash of %s is written, but %s could not be said: %v", user, AccountHashedEvent, err)
}
}
// NodeAccountArgs are `node-account`'s arguments: the joining node, the sealing key its machine presented (raw X25519 in
// base64, or SPKI PEM), the digest of the keys it presented, and whether to retire the account instead.
type NodeAccountArgs struct {
Node string
SealingKey string
KeysDigest string
Retire bool
}
// NodeAccountAnswer is mesh-sdk's meshvault.NodeAccountAnswer, the wire shape mesh-enrolment decodes and relays into the
// redeem answer: the login, and the account sealed to the presented key, as the node-engine opens it (the password,
// sealed anonymously to its raw X25519 key, in base64); or that it was retired.
type NodeAccountAnswer = meshvault.NodeAccountAnswer
// NodeAccount answers caller's `node-account`: the joining node's account, minted once for a node and a set of keys and
// answered sealed to the key presented; or, with retire, taken away again at a revoke.
func (v *Vault) NodeAccount(caller string, a NodeAccountArgs) (NodeAccountAnswer, error) {
if err := v.fromEnrolment(caller, "node-account"); err != nil {
return NodeAccountAnswer{}, err
}
if err := enrolNode(a.Node); err != nil {
return NodeAccountAnswer{}, err
}
if v.Accounts == nil {
return NodeAccountAnswer{}, errors.New("the vault has no state of bus accounts to write to")
}
user := NodeEnginePrefix + a.Node
secret := loginSecret(a.Node, user)
v.hashMu.Lock()
defer v.hashMu.Unlock()
was, err := v.Accounts.Get(user)
if err != nil {
return NodeAccountAnswer{}, fmt.Errorf("%s %s could not be read: %w", BusAccountsState, user, err)
}
if a.Retire {
return v.retireNodeAccount(caller, a.Node, user, secret, was)
}
if was != nil && (was.Kept != KeptNode || was.Adopted) {
return NodeAccountAnswer{}, fmt.Errorf("%s already has an account the vault did not make for an enrolment: a node's account is never "+
"minted anew (novox/hq ADR 0561 §4)", user)
}
if _, held, err := v.Store.Read(enrolSecret(a.Node)); err != nil {
return NodeAccountAnswer{}, err
} else if !held {
return NodeAccountAnswer{}, fmt.Errorf("%s holds no enrolment token, so no enrolment is under way: no account is made", a.Node)
}
to, err := sealingRaw(a.SealingKey)
if err != nil {
return NodeAccountAnswer{}, err
}
if strings.TrimSpace(a.KeysDigest) == "" {
return NodeAccountAnswer{}, errors.New("node-account names the digest of the keys the machine presented (`keys_digest`)")
}
if was == nil {
if err := v.mintNodeAccount(caller, a.Node, user, secret, a.KeysDigest); err != nil {
return NodeAccountAnswer{}, err
}
} else if was.KeysDigest != a.KeysDigest {
return NodeAccountAnswer{}, fmt.Errorf("%s was made for another set of keys than the ones presented now: revoke the enrolment and issue it again", user)
}
rec, held, err := v.Store.Read(secret)
if err != nil || !held {
return NodeAccountAnswer{}, fmt.Errorf("the vault holds no value of %s to answer: %v", user, err)
}
value, err := openAtRest(rec, v)
if err != nil {
return NodeAccountAnswer{}, fmt.Errorf("%s does not open with the vault's key: %w", secret, err)
}
var acct struct {
Password string `json:"password"`
}
if err := json.Unmarshal([]byte(value), &acct); err != nil || acct.Password == "" {
return NodeAccountAnswer{}, fmt.Errorf("%s holds no password to answer", secret)
}
sealed, err := operatorSeal(to, []byte(acct.Password))
if err != nil {
return NodeAccountAnswer{}, err
}
_ = v.recordHanding(Handing{To: caller, Name: secret, Generation: rec.Generation, Outcome: "handed", Why: "node account of a joining node, sealed to its presented key"})
return NodeAccountAnswer{Node: a.Node, Login: user, Account: sealed}, nil
}
// mintNodeAccount makes a joining node's account: its value kept as the login's own secret, its hash in bus-accounts, and
// the node's key in node-logins naming it in use with nothing rotating.
func (v *Vault) mintNodeAccount(caller, node, user, secret, digest string) error {
value, password, err := v.accountValue(node, user)
if err != nil {
return err
}
c, err := v.keepWith(secret, value, "the vault", caller, OriginMade, keepOptions{quiet: true})
if err != nil {
return fmt.Errorf("the account of %s is not kept: %w", node, err)
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), v.hashCost())
if err != nil {
return err
}
at := v.Now().UTC()
fp := Fingerprint(value)
if err := v.Accounts.Put(AccountHash{User: user, Hash: string(hash), Of: secret, Generation: c.Generation, Fingerprint: fp,
At: at, Kept: KeptNode, KeysDigest: digest}); err != nil {
return fmt.Errorf("the hash of %s could not be written to %s: %w", user, BusAccountsState, err)
}
v.saidHashed(user, secret, c.Generation, fp, at)
if err := v.putLogin(NodeLogin{Node: node, Login: user, Since: at}); err != nil {
return fmt.Errorf("the account of %s is made, and %s %s could not be written: %w", node, NodeLoginsState, node, err)
}
v.log("made the account %s of a joining node for %s", user, caller)
return nil
}
// accountValue is a node's login whole, in the shape the controller's accounts have (url, fingerprint, node, user,
// password), and its password.
func (v *Vault) accountValue(node, user string) (value, password string, err error) {
raw := make([]byte, 30)
if _, err := rand.Read(raw); err != nil {
return "", "", err
}
password = base64.RawURLEncoding.EncodeToString(raw)
acct := map[string]string{"node": node, "user": user, "password": password}
if v.TokenBase != nil {
if base, err := v.TokenBase(); err == nil {
acct["url"], acct["fingerprint"] = base.Broker, base.Fingerprint
}
}
out, err := json.Marshal(acct)
return string(out), password, err
}
// retireNodeAccount takes a joining node's account away: its hash, its value and its key in node-logins. Only one the
// vault made for an enrolment, and only while nothing rotates.
func (v *Vault) retireNodeAccount(caller, node, user, secret string, was *AccountHash) (NodeAccountAnswer, error) {
out := NodeAccountAnswer{Node: node, Login: user}
if was != nil && was.Kept != KeptNode {
return out, fmt.Errorf("%s is not an account the vault made for an enrolment: nothing was retired", user)
}
if nl, err := v.getLogin(node); err != nil {
return out, err
} else if nl != nil && nl.Rotating != "" {
return out, fmt.Errorf("%s is rotating its login: nothing was retired", node)
}
if was != nil {
if err := v.Accounts.Delete(user); err != nil {
return out, fmt.Errorf("the hash of %s could not be taken out of %s: %w", user, BusAccountsState, err)
}
out.Retired = true
}
if _, held, err := v.Store.Read(secret); err != nil {
return out, err
} else if held && was != nil {
if err := v.Store.Delete(secret); err != nil {
return out, fmt.Errorf("the value of %s could not be deleted: %w", user, err)
}
}
if out.Retired {
if err := v.deleteLogin(node); err != nil {
return out, err
}
_ = v.recordHanding(Handing{To: caller, Name: secret, Outcome: "retired", Why: "node account of an enrolment revoked"})
v.log("retired the account %s of a revoked enrolment, asked by %s", user, caller)
}
return out, nil
}
// sealingRaw takes the key a machine presents to be sealed to, as raw X25519 in base64 or SPKI PEM, and gives it raw in
// base64: the form the node-engine's sealing key opens an anonymous box with.
func sealingRaw(key string) (string, error) {
key = strings.TrimSpace(key)
if key == "" {
return "", errors.New("node-account names the sealing key the machine presented (`sealing_key`)")
}
if raw, err := base64.StdEncoding.DecodeString(key); err == nil && len(raw) == 32 {
return key, nil
}
block, _ := pem.Decode([]byte(key))
if block == nil {
return "", errors.New("the sealing key presented is neither a raw X25519 key in base64 nor SPKI PEM")
}
parsed, err := x509.ParsePKIXPublicKey(block.Bytes)
pub, ok := parsed.(*ecdh.PublicKey)
if err != nil || !ok || pub.Curve() != ecdh.X25519() {
return "", errors.New("the sealing key presented is not an X25519 key")
}
return base64.StdEncoding.EncodeToString(pub.Bytes()), nil
}
// sealingPEM takes the key a machine presents to be sealed to, as raw X25519 in base64 or SPKI PEM, and gives PEM.
func sealingPEM(key string) (string, error) {
key = strings.TrimSpace(key)
if key == "" {
return "", errors.New("node-account names the sealing key the machine presented (`sealing_key`)")
}
if strings.Contains(key, "BEGIN") {
return askerKey(key)
}
pem, err := publicPEM(key)
if err != nil {
return "", fmt.Errorf("the sealing key presented is not a usable key: %w", err)
}
return pem, nil
}
// TokenBaseFromFile reads what a token states of the bus from the vault's own account file (`{url, fingerprint}` as the
// controller's accounts have), in the directory of the vault's state. It answers an error when there is none.
func TokenBaseFromFile(stateDir string) func() (TokenBase, error) {
return func() (TokenBase, error) {
raw, err := os.ReadFile(filepath.Join(stateDir, "broker"))
if err != nil {
return TokenBase{}, fmt.Errorf("the vault's own account is not readable, so the bus's address is not known: %w", err)
}
var acct struct {
URL string `json:"url"`
Fingerprint string `json:"fingerprint"`
}
if json.Unmarshal(raw, &acct) != nil || acct.URL == "" || acct.Fingerprint == "" {
return TokenBase{}, errors.New("the vault's own account names no bus address and certificate fingerprint")
}
return TokenBase{Broker: acct.URL, Fingerprint: acct.Fingerprint}, nil
}
}