Files
sultan/pkg/auth/keys.go
T
br c8df9d7651 erste module drin.
zwischenstand: Architektur aktualisiert, TOFU implementiert,
2026-10-01 22:22:13 +02:00

107 lines
3.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
}