mirror of
https://github.com/postmannen/ctrl.git
synced 2024-12-14 12:37:31 +00:00
2fb43591ce
removed example for no longer existing relay messages cleaned up comments Removed some remaings after REQToFileNACK Implemented env variables for all flags, and removed config flag. Also added use of .env file. removed configuration as input argument from all the loggers replaced logging of new messages in read folder with a logDebug so we don't send those messages to the error kernel
474 lines
12 KiB
Go
474 lines
12 KiB
Go
package ctrl
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// nodeAuth is the structure that holds both keys and acl's
|
|
// that the running ctrl node shall use for authorization.
|
|
// It holds a mutex to use when interacting with the map.
|
|
type nodeAuth struct {
|
|
// ACL that defines where a node is allowed to recieve from.
|
|
nodeAcl *nodeAcl
|
|
|
|
// All the public keys for nodes a node is allowed to receive from.
|
|
publicKeys *publicKeys
|
|
|
|
// Full path to the signing keys folder
|
|
SignKeyFolder string
|
|
// Full path to private signing key.
|
|
SignKeyPrivateKeyPath string
|
|
// Full path to public signing key.
|
|
SignKeyPublicKeyPath string
|
|
|
|
// private key for ed25519 signing.
|
|
SignPrivateKey []byte
|
|
// public key for ed25519 signing.
|
|
SignPublicKey []byte
|
|
|
|
configuration *Configuration
|
|
|
|
errorKernel *errorKernel
|
|
}
|
|
|
|
func newNodeAuth(configuration *Configuration, errorKernel *errorKernel) *nodeAuth {
|
|
n := nodeAuth{
|
|
nodeAcl: newNodeAcl(configuration, errorKernel),
|
|
publicKeys: newPublicKeys(configuration, errorKernel),
|
|
configuration: configuration,
|
|
errorKernel: errorKernel,
|
|
}
|
|
|
|
// Set the signing key paths.
|
|
n.SignKeyFolder = filepath.Join(configuration.ConfigFolder, "signing")
|
|
n.SignKeyPrivateKeyPath = filepath.Join(n.SignKeyFolder, "private.key")
|
|
n.SignKeyPublicKeyPath = filepath.Join(n.SignKeyFolder, "public.key")
|
|
|
|
err := n.loadSigningKeys()
|
|
if err != nil {
|
|
er := fmt.Errorf("newNodeAuth: %v", err)
|
|
errorKernel.logError(er)
|
|
os.Exit(1)
|
|
}
|
|
|
|
return &n
|
|
}
|
|
|
|
// --------------------- ACL ---------------------
|
|
|
|
type aclAndHash struct {
|
|
Acl map[Node]map[command]struct{}
|
|
Hash [32]byte
|
|
}
|
|
|
|
func newAclAndHash() aclAndHash {
|
|
a := aclAndHash{
|
|
Acl: make(map[Node]map[command]struct{}),
|
|
}
|
|
|
|
return a
|
|
}
|
|
|
|
type nodeAcl struct {
|
|
// allowed is a map for holding all the allowed signatures.
|
|
aclAndHash aclAndHash
|
|
filePath string
|
|
mu sync.Mutex
|
|
errorKernel *errorKernel
|
|
configuration *Configuration
|
|
}
|
|
|
|
func newNodeAcl(c *Configuration, errorKernel *errorKernel) *nodeAcl {
|
|
n := nodeAcl{
|
|
aclAndHash: newAclAndHash(),
|
|
filePath: filepath.Join(c.DatabaseFolder, "node_aclmap.txt"),
|
|
errorKernel: errorKernel,
|
|
configuration: c,
|
|
}
|
|
|
|
err := n.loadFromFile()
|
|
if err != nil {
|
|
er := fmt.Errorf("error: newNodeAcl: loading acl's from file: %v", err)
|
|
errorKernel.logError(er)
|
|
// os.Exit(1)
|
|
}
|
|
|
|
return &n
|
|
}
|
|
|
|
// loadFromFile will try to load all the currently stored acl's from file,
|
|
// and return the error if it fails.
|
|
// If no file is found a nil error is returned.
|
|
func (n *nodeAcl) loadFromFile() error {
|
|
if _, err := os.Stat(n.filePath); os.IsNotExist(err) {
|
|
// Just logging the error since it is not crucial that a key file is missing,
|
|
// since a new one will be created on the next update.
|
|
er := fmt.Errorf("acl: loadFromFile: no acl file found at %v", n.filePath)
|
|
n.errorKernel.logDebug(er)
|
|
return nil
|
|
}
|
|
|
|
fh, err := os.OpenFile(n.filePath, os.O_RDONLY, 0660)
|
|
if err != nil {
|
|
return fmt.Errorf("error: failed to open acl file: %v", err)
|
|
}
|
|
defer fh.Close()
|
|
|
|
b, err := io.ReadAll(fh)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
n.mu.Lock()
|
|
defer n.mu.Unlock()
|
|
err = json.Unmarshal(b, &n.aclAndHash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
er := fmt.Errorf("nodeAcl: loadFromFile: Loaded existing acl's from file: %v", n.aclAndHash.Hash)
|
|
n.errorKernel.logDebug(er)
|
|
|
|
return nil
|
|
}
|
|
|
|
// saveToFile will save the acl to file for persistent storage.
|
|
// An error is returned if it fails.
|
|
func (n *nodeAcl) saveToFile() error {
|
|
fh, err := os.OpenFile(n.filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0660)
|
|
if err != nil {
|
|
return fmt.Errorf("error: failed to acl file: %v", err)
|
|
}
|
|
defer fh.Close()
|
|
|
|
n.mu.Lock()
|
|
defer n.mu.Unlock()
|
|
|
|
enc := json.NewEncoder(fh)
|
|
enc.SetEscapeHTML(false)
|
|
err = enc.Encode(n.aclAndHash)
|
|
|
|
// HERE
|
|
// b, err := json.Marshal(n.aclAndHash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// _, err = fh.Write(b)
|
|
// if err != nil {
|
|
// return err
|
|
// }
|
|
|
|
return nil
|
|
}
|
|
|
|
// --------------------- KEYS ---------------------
|
|
|
|
type keysAndHash struct {
|
|
Keys map[Node][]byte
|
|
Hash [32]byte
|
|
}
|
|
|
|
func newKeysAndHash() *keysAndHash {
|
|
kh := keysAndHash{
|
|
Keys: make(map[Node][]byte),
|
|
}
|
|
return &kh
|
|
}
|
|
|
|
type publicKeys struct {
|
|
keysAndHash *keysAndHash
|
|
mu sync.Mutex
|
|
filePath string
|
|
errorKernel *errorKernel
|
|
configuration *Configuration
|
|
}
|
|
|
|
func newPublicKeys(c *Configuration, errorKernel *errorKernel) *publicKeys {
|
|
p := publicKeys{
|
|
keysAndHash: newKeysAndHash(),
|
|
filePath: filepath.Join(c.DatabaseFolder, "publickeys.txt"),
|
|
errorKernel: errorKernel,
|
|
configuration: c,
|
|
}
|
|
|
|
err := p.loadFromFile()
|
|
if err != nil {
|
|
er := fmt.Errorf("error: newPublicKeys: loading public keys from file: %v", err)
|
|
errorKernel.logError(er)
|
|
// os.Exit(1)
|
|
}
|
|
|
|
return &p
|
|
}
|
|
|
|
// loadFromFile will try to load all the currently stored public keys from file,
|
|
// and return the error if it fails.
|
|
// If no file is found a nil error is returned.
|
|
func (p *publicKeys) loadFromFile() error {
|
|
if _, err := os.Stat(p.filePath); os.IsNotExist(err) {
|
|
// Just logging the error since it is not crucial that a key file is missing,
|
|
// since a new one will be created on the next update.
|
|
er := fmt.Errorf("no public keys file found at %v, new file will be created", p.filePath)
|
|
p.errorKernel.logInfo(er)
|
|
return nil
|
|
}
|
|
|
|
fh, err := os.OpenFile(p.filePath, os.O_RDONLY, 0660)
|
|
if err != nil {
|
|
return fmt.Errorf("error: failed to open public keys file: %v", err)
|
|
}
|
|
defer fh.Close()
|
|
|
|
b, err := io.ReadAll(fh)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
err = json.Unmarshal(b, &p.keysAndHash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
er := fmt.Errorf("nodeAuth: loadFromFile: Loaded existing keys from file: %v", p.keysAndHash.Hash)
|
|
p.errorKernel.logDebug(er)
|
|
|
|
return nil
|
|
}
|
|
|
|
// saveToFile will save all the public kets to file for persistent storage.
|
|
// An error is returned if it fails.
|
|
func (p *publicKeys) saveToFile() error {
|
|
fh, err := os.OpenFile(p.filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0660)
|
|
if err != nil {
|
|
return fmt.Errorf("error: failed to open public keys file: %v", err)
|
|
}
|
|
defer fh.Close()
|
|
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
b, err := json.Marshal(p.keysAndHash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = fh.Write(b)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// loadSigningKeys will try to load the ed25519 signing keys. If the
|
|
// files are not found new keys will be generated and written to disk.
|
|
func (n *nodeAuth) loadSigningKeys() error {
|
|
// Check if folder structure exist, if not create it.
|
|
if _, err := os.Stat(n.SignKeyFolder); os.IsNotExist(err) {
|
|
err := os.MkdirAll(n.SignKeyFolder, 0770)
|
|
if err != nil {
|
|
er := fmt.Errorf("error: failed to create directory for signing keys : %v", err)
|
|
return er
|
|
}
|
|
|
|
}
|
|
|
|
// Check if there already are any keys in the etc folder.
|
|
foundKey := false
|
|
|
|
if _, err := os.Stat(n.SignKeyPublicKeyPath); !os.IsNotExist(err) {
|
|
foundKey = true
|
|
}
|
|
if _, err := os.Stat(n.SignKeyPrivateKeyPath); !os.IsNotExist(err) {
|
|
foundKey = true
|
|
}
|
|
|
|
// If no keys where found generete a new pair, load them into the
|
|
// processes struct fields, and write them to disk.
|
|
if !foundKey {
|
|
pub, priv, err := ed25519.GenerateKey(nil)
|
|
if err != nil {
|
|
er := fmt.Errorf("error: failed to generate ed25519 keys for signing: %v", err)
|
|
return er
|
|
}
|
|
pubB64string := base64.RawStdEncoding.EncodeToString(pub)
|
|
privB64string := base64.RawStdEncoding.EncodeToString(priv)
|
|
|
|
// Write public key to file.
|
|
err = n.writeSigningKey(n.SignKeyPublicKeyPath, pubB64string)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Write private key to file.
|
|
err = n.writeSigningKey(n.SignKeyPrivateKeyPath, privB64string)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Also store the keys in the processes structure so we can
|
|
// reference them from there when we need them.
|
|
n.SignPublicKey = pub
|
|
n.SignPrivateKey = priv
|
|
|
|
er := fmt.Errorf("info: no signing keys found, generating new keys")
|
|
n.errorKernel.logInfo(er)
|
|
|
|
// We got the new generated keys now, so we can return.
|
|
return nil
|
|
}
|
|
|
|
// Key files found, load them into the processes struct fields.
|
|
pubKey, _, err := n.readKeyFile(n.SignKeyPublicKeyPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n.SignPublicKey = pubKey
|
|
|
|
privKey, _, err := n.readKeyFile(n.SignKeyPrivateKeyPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n.SignPublicKey = pubKey
|
|
n.SignPrivateKey = privKey
|
|
|
|
return nil
|
|
}
|
|
|
|
// writeSigningKey will write the base64 encoded signing key to file.
|
|
func (n *nodeAuth) writeSigningKey(realPath string, keyB64 string) error {
|
|
fh, err := os.OpenFile(realPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0660)
|
|
if err != nil {
|
|
er := fmt.Errorf("error: failed to open key file for writing: %v", err)
|
|
return er
|
|
}
|
|
defer fh.Close()
|
|
|
|
_, err = fh.Write([]byte(keyB64))
|
|
if err != nil {
|
|
er := fmt.Errorf("error: failed to write key to file: %v", err)
|
|
return er
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// readKeyFile will take the path of a key file as input, read the base64
|
|
// encoded data, decode the data. It will return the raw data as []byte,
|
|
// the base64 encoded data, and any eventual error.
|
|
func (n *nodeAuth) readKeyFile(keyFile string) (ed2519key []byte, b64Key []byte, err error) {
|
|
fh, err := os.Open(keyFile)
|
|
if err != nil {
|
|
er := fmt.Errorf("error: failed to open key file: %v", err)
|
|
return nil, nil, er
|
|
}
|
|
defer fh.Close()
|
|
|
|
b, err := io.ReadAll(fh)
|
|
if err != nil {
|
|
er := fmt.Errorf("error: failed to read key file: %v", err)
|
|
return nil, nil, er
|
|
}
|
|
|
|
key, err := base64.RawStdEncoding.DecodeString(string(b))
|
|
if err != nil {
|
|
er := fmt.Errorf("error: failed to base64 decode key data: %v", err)
|
|
return nil, nil, er
|
|
}
|
|
|
|
return key, b, nil
|
|
}
|
|
|
|
// verifySignature
|
|
func (n *nodeAuth) verifySignature(m Message) bool {
|
|
// NB: Only enable signature checking for REQCliCommand for now.
|
|
if m.Method != REQCliCommand {
|
|
er := fmt.Errorf("verifySignature: not REQCliCommand and will not do signature check, method: %v", m.Method)
|
|
n.errorKernel.logInfo(er)
|
|
return true
|
|
}
|
|
|
|
// Verify if the signature matches.
|
|
argsStringified := argsToString(m.MethodArgs)
|
|
var ok bool
|
|
|
|
err := func() error {
|
|
n.publicKeys.mu.Lock()
|
|
pubKey := n.publicKeys.keysAndHash.Keys[m.FromNode]
|
|
if len(pubKey) != 32 {
|
|
err := fmt.Errorf("length of publicKey not equal to 32: %v", len(pubKey))
|
|
return err
|
|
}
|
|
|
|
ok = ed25519.Verify(pubKey, []byte(argsStringified), m.ArgSignature)
|
|
n.publicKeys.mu.Unlock()
|
|
|
|
return nil
|
|
}()
|
|
|
|
if err != nil {
|
|
n.errorKernel.logError(err)
|
|
}
|
|
|
|
er := fmt.Errorf("info: verifySignature, result: %v, fromNode: %v, method: %v", ok, m.FromNode, m.Method)
|
|
n.errorKernel.logInfo(er)
|
|
|
|
return ok
|
|
}
|
|
|
|
// verifyAcl
|
|
func (n *nodeAuth) verifyAcl(m Message) bool {
|
|
// NB: Only enable acl checking for REQCliCommand for now.
|
|
if m.Method != REQCliCommand {
|
|
er := fmt.Errorf("verifyAcl: not REQCliCommand and will not do acl check, method: %v", m.Method)
|
|
n.errorKernel.logInfo(er)
|
|
return true
|
|
}
|
|
|
|
argsStringified := argsToString(m.MethodArgs)
|
|
|
|
// Verify if the command matches the one in the acl map.
|
|
n.nodeAcl.mu.Lock()
|
|
defer n.nodeAcl.mu.Unlock()
|
|
|
|
cmdMap, ok := n.nodeAcl.aclAndHash.Acl[m.FromNode]
|
|
if !ok {
|
|
er := fmt.Errorf("verifyAcl: The fromNode=%v was not found in the acl", m.FromNode)
|
|
n.errorKernel.logError(er)
|
|
return false
|
|
}
|
|
|
|
_, ok = cmdMap[command("*")]
|
|
if ok {
|
|
er := fmt.Errorf("verifyAcl: The acl said \"*\", all commands allowed from node=%v", m.FromNode)
|
|
n.errorKernel.logInfo(er)
|
|
return true
|
|
}
|
|
|
|
_, ok = cmdMap[command(argsStringified)]
|
|
if !ok {
|
|
er := fmt.Errorf("verifyAcl: The command=%v was NOT FOUND in the acl", m.MethodArgs)
|
|
n.errorKernel.logInfo(er)
|
|
return false
|
|
}
|
|
|
|
er := fmt.Errorf("verifyAcl: the command was FOUND in the acl, verifyAcl, result: %v, fromNode: %v, method: %v", ok, m.FromNode, m.Method)
|
|
n.errorKernel.logInfo(er)
|
|
|
|
return true
|
|
}
|
|
|
|
// argsToString takes args in the format of []string and returns a string.
|
|
func argsToString(args []string) string {
|
|
return strings.Join(args, " ")
|
|
}
|