erste module drin.

zwischenstand: Architektur aktualisiert, TOFU implementiert,
This commit is contained in:
br committed 2026-10-01 22:22:13 +02:00
1 parent 9d5789c5d7
commit c8df9d7651
41 files changed
+6455 -36

No files matched your search

+106
View File
@@ -0,0 +1,106 @@
// 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
}