169 lines
6.4 KiB
Go
169 lines
6.4 KiB
Go
package main
|
|
|
|
// The keys the vault meets, and the one it holds (novox/hq ADR 0113 §2, ADR 0206).
|
|
//
|
|
// - Its own key: an X25519 pair made once in its state directory, the private half never leaving it. Every
|
|
// secret is kept sealed to it at rest.
|
|
// - An asker's key: the public half only, as SPKI PEM — the form of the ADR 0206 box (mesh-sdk go/seal).
|
|
// A raw 32-byte key is refused, because a raw private key looks the same and the vault takes none.
|
|
// - The operator key: the public half, taken as SPKI PEM only and kept raw in base64, the form `operator key
|
|
// make` writes and the NaCl box the operator opens uses.
|
|
//
|
|
// No private key is ever accepted: not as a key to seal to, not as a secret to hold.
|
|
|
|
import (
|
|
"crypto/ecdh"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/seal"
|
|
"golang.org/x/crypto/nacl/box"
|
|
)
|
|
|
|
// OwnKeyFile is where the vault's own key lives in its state directory.
|
|
const OwnKeyFile = "vault-key.json"
|
|
|
|
// OwnKey is the vault's own key pair, made once in dir. Written whole and synced, then linked into place and
|
|
// the directory synced, so a second process reads the first's key and never half of it.
|
|
//
|
|
// **A key is made only for an empty store.** When the key file is missing and the store holds secrets, the
|
|
// key they are sealed to is lost or misplaced, and a new key would open none of them: the vault refuses to
|
|
// start, loudly, rather than seal new secrets to a key beside old ones nothing opens. The operator copies
|
|
// are what re-raise it (ADR 0113 §21).
|
|
func OwnKey(dir string, store *Store) (seal.KeyPair, error) {
|
|
if dir == "" {
|
|
return seal.KeyPair{}, errors.New("the vault has no state directory: MESH_VAULT_STATE is not set")
|
|
}
|
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
|
return seal.KeyPair{}, err
|
|
}
|
|
file := filepath.Join(dir, OwnKeyFile)
|
|
if _, err := os.Stat(file); errors.Is(err, os.ErrNotExist) {
|
|
if store != nil {
|
|
empty, err := store.Empty()
|
|
if err != nil {
|
|
return seal.KeyPair{}, fmt.Errorf("the vault's key is missing and its store cannot be read: %w", err)
|
|
}
|
|
if !empty {
|
|
return seal.KeyPair{}, fmt.Errorf("THE VAULT'S KEY IS MISSING (%s) WHILE ITS STORE HOLDS SECRETS SEALED TO IT: "+
|
|
"the vault does not start and makes no new key, which would open none of them — put the key back, "+
|
|
"or re-raise the vault from the operator copies (novox/hq ADR 0113 §21)", file)
|
|
}
|
|
}
|
|
k, err := seal.GenerateKeyPair()
|
|
if err != nil {
|
|
return seal.KeyPair{}, err
|
|
}
|
|
raw, _ := json.Marshal(k)
|
|
tmp := filepath.Join(dir, fmt.Sprintf(".vault-key.%d.json", os.Getpid()))
|
|
if err := writeSynced(tmp, raw); err != nil {
|
|
return seal.KeyPair{}, err
|
|
}
|
|
_ = os.Link(tmp, file) // fails when another made it first, which is right
|
|
_ = os.Remove(tmp)
|
|
if err := syncDir(dir); err != nil {
|
|
return seal.KeyPair{}, err
|
|
}
|
|
}
|
|
raw, err := os.ReadFile(file)
|
|
if err != nil {
|
|
return seal.KeyPair{}, err
|
|
}
|
|
var k seal.KeyPair
|
|
if err := json.Unmarshal(raw, &k); err != nil || k.PublicKey == "" || k.PrivateKey == "" {
|
|
return seal.KeyPair{}, fmt.Errorf("%s is not the vault's key", file)
|
|
}
|
|
return k, nil
|
|
}
|
|
|
|
var privateKeyPEM = regexp.MustCompile(`-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----`)
|
|
|
|
// privateKeyIn says whether text carries a private key.
|
|
func privateKeyIn(text string) bool {
|
|
return privateKeyPEM.MatchString(text) || strings.Contains(text, "PRIVATE KEY")
|
|
}
|
|
|
|
// askerKey is an asker's public key, checked and as PEM: SPKI, X25519, and never a private key.
|
|
func askerKey(key string) (string, error) {
|
|
if privateKeyIn(key) {
|
|
return "", errors.New("that is a private key, and a private key is never sent to the vault (ADR 0113 §2): send the public half")
|
|
}
|
|
block, rest := pem.Decode([]byte(strings.TrimSpace(key)))
|
|
if block == nil || block.Type != "PUBLIC KEY" || strings.TrimSpace(string(rest)) != "" {
|
|
return "", errors.New("the key to seal to is one X25519 public key as SPKI PEM (`-----BEGIN PUBLIC KEY-----`), the form of the sealed box (ADR 0206)")
|
|
}
|
|
k, err := x509.ParsePKIXPublicKey(block.Bytes)
|
|
if err != nil {
|
|
return "", fmt.Errorf("the key to seal to is not a public key: %w", err)
|
|
}
|
|
pub, ok := k.(*ecdh.PublicKey)
|
|
if !ok || pub.Curve() != ecdh.X25519() {
|
|
return "", errors.New("the key to seal to is not an X25519 key")
|
|
}
|
|
return string(pem.EncodeToMemory(block)), nil
|
|
}
|
|
|
|
// checkOperatorKey takes the operator's public key as SPKI PEM — never a private key, and never a raw key,
|
|
// which cannot be told from a private one — and gives it in the raw base64 form the operator's NaCl box and
|
|
// `secret recover` use.
|
|
func checkOperatorKey(key string) (string, error) {
|
|
if privateKeyIn(key) {
|
|
return "", errors.New("that is a private key: the operator key's private half never enters the mesh (ADR 0113 §2)")
|
|
}
|
|
block, rest := pem.Decode([]byte(strings.TrimSpace(key)))
|
|
if block == nil || block.Type != "PUBLIC KEY" || strings.TrimSpace(string(rest)) != "" {
|
|
return "", errors.New("the operator key is one X25519 public key as SPKI PEM (`-----BEGIN PUBLIC KEY-----`); " +
|
|
"a raw key is refused, because a raw private key looks the same")
|
|
}
|
|
k, err := x509.ParsePKIXPublicKey(block.Bytes)
|
|
if err != nil {
|
|
return "", fmt.Errorf("the operator key is not a public key: %w", err)
|
|
}
|
|
pub, ok := k.(*ecdh.PublicKey)
|
|
if !ok || pub.Curve() != ecdh.X25519() {
|
|
return "", errors.New("the operator key is not an X25519 key")
|
|
}
|
|
return base64.StdEncoding.EncodeToString(pub.Bytes()), nil
|
|
}
|
|
|
|
// publicPEM wraps a raw X25519 public key, base64, as SPKI PEM.
|
|
func publicPEM(raw string) (string, error) {
|
|
b, err := base64.StdEncoding.DecodeString(strings.TrimSpace(raw))
|
|
if err != nil || len(b) != 32 {
|
|
return "", errors.New("not a raw X25519 key in base64")
|
|
}
|
|
pub, err := ecdh.X25519().NewPublicKey(b)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
der, err := x509.MarshalPKIXPublicKey(pub)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: der})), nil
|
|
}
|
|
|
|
// operatorSeal seals value to the operator key in the box the operator opens today.
|
|
func operatorSeal(key string, value []byte) (string, error) {
|
|
raw, err := base64.StdEncoding.DecodeString(key)
|
|
if err != nil || len(raw) != 32 {
|
|
return "", errors.New("the operator key is not usable")
|
|
}
|
|
var pub [32]byte
|
|
copy(pub[:], raw)
|
|
sealed, err := box.SealAnonymous(nil, value, &pub, rand.Reader)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return base64.StdEncoding.EncodeToString(sealed), nil
|
|
}
|