// Package auth authentifiziert Agent-Requests per Ed25519-Signatur (Spec // Abschnitt 3.2) – in jedem Tier, auch im Community-/Homelab-Betrieb. // // Der Agent signiert jeden Request mit seinem lokal erzeugten node.key // (Signer). Der Server prüft Signatur, Zeitfenster und Nonce, schlägt den // Knoten über den Public Key nach und liefert dessen Identität (Verifier). // Die Knoten-ID stammt damit immer aus dem geprüften Schlüssel, nie aus einer // Angabe des Clients. // // Beide Seiten verweigern Klartext: Der Signer signiert nur https-Requests, // der Verifier akzeptiert nur Requests, die über TLS angekommen sind. package auth import ( "crypto/ed25519" "crypto/rand" "crypto/x509" "encoding/base64" "encoding/pem" "errors" "fmt" "os" ) var ( // ErrInvalidKey: Schlüssel fehlt, ist kaputt oder kein Ed25519-Schlüssel. ErrInvalidKey = errors.New("auth: ungültiger Schlüssel") // ErrInsecureKeyFile: Die Schlüsseldatei ist für Gruppe oder andere lesbar. ErrInsecureKeyFile = errors.New("auth: Schlüsseldatei hat zu offene Rechte (erwartet 0600)") ) const pemTypePrivateKey = "PRIVATE KEY" // GenerateKey erzeugt ein neues Ed25519-Schlüsselpaar. func GenerateKey() (ed25519.PrivateKey, error) { _, priv, err := ed25519.GenerateKey(rand.Reader) return priv, err } // WritePrivateKey speichert key als PKCS#8-PEM mit Rechten 0600. Eine // bestehende Datei wird nie überschrieben (os.ErrExist) – ein versehentlich // ersetzter Schlüssel würde den Knoten aussperren. func WritePrivateKey(path string, key ed25519.PrivateKey) error { der, err := x509.MarshalPKCS8PrivateKey(key) if err != nil { return fmt.Errorf("%w: %v", ErrInvalidKey, err) } f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) if err != nil { return err } if err := pem.Encode(f, &pem.Block{Type: pemTypePrivateKey, Bytes: der}); err != nil { f.Close() os.Remove(path) return err } if err := f.Close(); err != nil { os.Remove(path) return err } return nil } // ReadPrivateKey liest einen mit WritePrivateKey gespeicherten Schlüssel. // Dateien, die Gruppe oder andere lesen dürfen, werden abgelehnt. func ReadPrivateKey(path string) (ed25519.PrivateKey, error) { info, err := os.Stat(path) if err != nil { return nil, err } if info.Mode().Perm()&0o077 != 0 { return nil, fmt.Errorf("%w: %s hat %o", ErrInsecureKeyFile, path, info.Mode().Perm()) } data, err := os.ReadFile(path) if err != nil { return nil, err } block, _ := pem.Decode(data) if block == nil || block.Type != pemTypePrivateKey { return nil, fmt.Errorf("%w: kein %q-PEM-Block in %s", ErrInvalidKey, pemTypePrivateKey, path) } parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { return nil, fmt.Errorf("%w: %v", ErrInvalidKey, err) } key, ok := parsed.(ed25519.PrivateKey) if !ok { return nil, fmt.Errorf("%w: %T statt Ed25519", ErrInvalidKey, parsed) } return key, nil } // EncodePublicKey kodiert einen Public Key so, wie er in Headern und im // Store (nodes.public_key) steht: Base64 (Standard-Alphabet). func EncodePublicKey(pub ed25519.PublicKey) string { return base64.StdEncoding.EncodeToString(pub) } // DecodePublicKey ist die Umkehrung von EncodePublicKey. func DecodePublicKey(s string) (ed25519.PublicKey, error) { b, err := base64.StdEncoding.DecodeString(s) if err != nil || len(b) != ed25519.PublicKeySize { return nil, fmt.Errorf("%w: Public Key nicht lesbar", ErrInvalidKey) } return ed25519.PublicKey(b), nil }