The seat mesh-enrolment's tokens answers the token sealed to the operator key; the operator opens it at their own terminal. The subcommand reads the sealed token on standard input and the private key from a file, prints the token text a joining machine takes, asks nothing of the node-engine's socket, and says the token is not sealed to a wrong key (ADR 0587 §6, §8). New: one table-style test function; mesh-cli had 6, so none was merged or deleted. (issue 774, 774/S51) Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
98 lines
3.5 KiB
Go
98 lines
3.5 KiB
Go
package main
|
|
|
|
// `nox enrol open`: the operator opens an enrolment token at their own terminal (novox/hq ADR 0587 §6, §8).
|
|
//
|
|
// The seat mesh-enrolment's `tokens` answers the token sealed to the operator key, which only the operator's private key
|
|
// opens; the mesh never holds it. This subcommand is local: it asks nothing of the node-engine's socket and nothing of
|
|
// the controller or the bus. It reads the sealed token (the base64 of the sealed box) on standard input and the private
|
|
// key from a file (`--key`, the raw X25519 key in base64 that `operator key make` wrote), opens the box, and prints the
|
|
// token text a joining machine's `enrol` takes. The key is read from the file named and nowhere else, and never printed.
|
|
|
|
import (
|
|
"crypto/ecdh"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
|
|
"golang.org/x/crypto/nacl/box"
|
|
)
|
|
|
|
const enrolUsage = `nox enrol open --key <operator private key file>
|
|
|
|
Opens an enrolment token sealed to the operator key (the answer of the seat mesh-enrolment's tokens) and prints the token
|
|
to type on the new machine. The sealed token is read on standard input; this runs here and asks nothing of the mesh.
|
|
`
|
|
|
|
// maxSealedToken bounds what is read of standard input: a token is a few hundred bytes.
|
|
const maxSealedToken = 16 << 10
|
|
|
|
// enrolOpen runs `enrol open`: args are what follows `enrol open`.
|
|
func enrolOpen(args []string, stdin io.Reader, stdinIsTerminal bool, stdout, stderr io.Writer) int {
|
|
keyFile := ""
|
|
for i := 0; i < len(args); i++ {
|
|
switch {
|
|
case args[i] == "--key" && i+1 < len(args):
|
|
keyFile = args[i+1]
|
|
i++
|
|
case strings.HasPrefix(args[i], "--key="):
|
|
keyFile = strings.TrimPrefix(args[i], "--key=")
|
|
default:
|
|
fmt.Fprint(stderr, enrolUsage)
|
|
return 2
|
|
}
|
|
}
|
|
if keyFile == "" {
|
|
fmt.Fprint(stderr, enrolUsage)
|
|
return 2
|
|
}
|
|
if stdinIsTerminal {
|
|
fmt.Fprintln(stderr, "mesh-cli: give the sealed token on standard input (a pipe or a file); nothing was opened")
|
|
return 2
|
|
}
|
|
raw, err := os.ReadFile(keyFile)
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "mesh-cli: the operator key could not be read: %v\n", err)
|
|
return 1
|
|
}
|
|
sealed, err := io.ReadAll(io.LimitReader(stdin, maxSealedToken+1))
|
|
if err != nil || len(sealed) > maxSealedToken {
|
|
fmt.Fprintln(stderr, "mesh-cli: the sealed token could not be read from standard input, or is longer than a token is")
|
|
return 1
|
|
}
|
|
text, err := openSealed(strings.TrimSpace(string(raw)), strings.TrimSpace(string(sealed)))
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "mesh-cli: %v\n", err)
|
|
return 1
|
|
}
|
|
fmt.Fprintln(stdout, text)
|
|
return 0
|
|
}
|
|
|
|
// openSealed opens a value sealed to the operator key (the sealed box of mesh-vault's operator copies, box.SealAnonymous
|
|
// in base64) with the private key, the raw X25519 key in base64.
|
|
func openSealed(privateKey, sealed string) (string, error) {
|
|
private, err := base64.StdEncoding.DecodeString(privateKey)
|
|
if err != nil || len(private) != 32 {
|
|
return "", errors.New("that is not an operator private key (the raw X25519 key in base64)")
|
|
}
|
|
key, err := ecdh.X25519().NewPrivateKey(private)
|
|
if err != nil {
|
|
return "", fmt.Errorf("that is not a usable operator private key: %w", err)
|
|
}
|
|
blob, err := base64.StdEncoding.DecodeString(sealed)
|
|
if err != nil {
|
|
return "", fmt.Errorf("this is not a sealed token: %w", err)
|
|
}
|
|
var pub, priv [32]byte
|
|
copy(pub[:], key.PublicKey().Bytes())
|
|
copy(priv[:], private)
|
|
out, ok := box.OpenAnonymous(nil, blob, &pub, &priv)
|
|
if !ok {
|
|
return "", errors.New("this token is not sealed to that key; nothing is printed")
|
|
}
|
|
return strings.TrimSpace(string(out)), nil
|
|
}
|