107 lines
3.4 KiB
Go
107 lines
3.4 KiB
Go
// 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
|
||
}
|