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 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 }