Files
br ca6cdcca63 software steht so weit.
jetzt build + docker build und testen
2026-10-02 13:25:34 +02:00

192 lines
5.8 KiB
Go
Raw Permalink 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 crypto stellt die Transportverschlüsselung zwischen Agent und
// Server sicher (Spec Abschnitt 3.1):
//
// - Server: selbstsigniertes ECDSA-P-256-Zertifikat, einmalig erzeugt und
// dauerhaft im Datenverzeichnis abgelegt; nur TLS 1.3.
// - Agent: Zertifikats-Pinning über den SHA-256-Fingerprint, unabhängig
// von der Adresse. Einen blinden Erstkontakt gibt es nicht: Der
// Fingerprint kommt aus der Einladung, danach aus node.json.
// - Enrollment-Token: sltn_enroll_<secret>.<fingerprint>[.<server-url>].
// Der Server speichert nur das Secret (gehasht), der Agent nutzt
// Fingerprint und Adresse für den Erstkontakt.
package crypto
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"math/big"
"os"
"path/filepath"
"time"
)
const (
serverKeyFile = "server.key"
serverCertFile = "server.crt"
certValidity = 20 * 365 * 24 * time.Hour
)
var (
// ErrIncompleteCert: Nur Schlüssel oder nur Zertifikat vorhanden. Es
// wird bewusst nichts neu erzeugt, weil das alle Pins der Agents brechen
// würde.
ErrIncompleteCert = errors.New("crypto: server.key und server.crt müssen beide vorhanden sein oder beide fehlen")
// ErrInsecureKeyFile: server.key ist für Gruppe oder andere lesbar.
ErrInsecureKeyFile = errors.New("crypto: server.key hat zu offene Rechte (erwartet 0600)")
// ErrInvalidFingerprint: Kein gültiger Fingerprint.
ErrInvalidFingerprint = errors.New("crypto: ungültiger Fingerprint")
)
// Fingerprint ist der SHA-256 des DER-kodierten Zertifikats, Base64url ohne
// Padding (43 Zeichen) – kompakt genug für den Enrollment-Token.
type Fingerprint string
// FingerprintOf berechnet den Fingerprint eines Zertifikats.
func FingerprintOf(cert *x509.Certificate) Fingerprint {
sum := sha256.Sum256(cert.Raw)
return Fingerprint(base64.RawURLEncoding.EncodeToString(sum[:]))
}
// ParseFingerprint prüft und übernimmt einen Fingerprint.
func ParseFingerprint(s string) (Fingerprint, error) {
b, err := base64.RawURLEncoding.DecodeString(s)
if err != nil || len(b) != sha256.Size {
return "", fmt.Errorf("%w: %q", ErrInvalidFingerprint, s)
}
return Fingerprint(s), nil
}
// ServerCert ist das TLS-Zertifikat des Servers samt Fingerprint.
type ServerCert struct {
Certificate tls.Certificate
Fingerprint Fingerprint
}
// Leaf liefert das geparste Zertifikat.
func (s ServerCert) Leaf() *x509.Certificate { return s.Certificate.Leaf }
// LoadOrCreateServerCert lädt server.key/server.crt aus dir oder erzeugt sie
// beim allerersten Start. Das Zertifikat ist 20 Jahre gültig: Agents pinnen
// den Fingerprint, ein Wechsel würde alle aussperren.
func LoadOrCreateServerCert(dir string) (ServerCert, error) {
keyPath, certPath := filepath.Join(dir, serverKeyFile), filepath.Join(dir, serverCertFile)
keyExists, err := exists(keyPath)
if err != nil {
return ServerCert{}, err
}
certExists, err := exists(certPath)
if err != nil {
return ServerCert{}, err
}
switch {
case keyExists && certExists:
case !keyExists && !certExists:
if err := createServerCert(keyPath, certPath); err != nil {
return ServerCert{}, err
}
default:
return ServerCert{}, fmt.Errorf("%w (in %s)", ErrIncompleteCert, dir)
}
info, err := os.Stat(keyPath)
if err != nil {
return ServerCert{}, err
}
if info.Mode().Perm()&0o077 != 0 {
return ServerCert{}, fmt.Errorf("%w: %s hat %o", ErrInsecureKeyFile, keyPath, info.Mode().Perm())
}
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return ServerCert{}, fmt.Errorf("crypto: Server-Zertifikat laden: %w", err)
}
if cert.Leaf == nil {
if cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]); err != nil {
return ServerCert{}, err
}
}
return ServerCert{Certificate: cert, Fingerprint: FingerprintOf(cert.Leaf)}, nil
}
func exists(path string) (bool, error) {
_, err := os.Stat(path)
if errors.Is(err, os.ErrNotExist) {
return false, nil
}
return err == nil, err
}
func createServerCert(keyPath, certPath string) error {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return err
}
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil {
return err
}
now := time.Now()
tmpl := &x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{CommonName: "sultan-server"},
NotBefore: now.Add(-time.Hour),
NotAfter: now.Add(certValidity),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
if err != nil {
return err
}
keyDER, err := x509.MarshalPKCS8PrivateKey(key)
if err != nil {
return err
}
if err := writeNew(keyPath, 0o600, &pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}); err != nil {
return err
}
if err := writeNew(certPath, 0o644, &pem.Block{Type: "CERTIFICATE", Bytes: der}); err != nil {
os.Remove(keyPath)
return err
}
return nil
}
// writeNew legt path neu an (nie überschreiben) und setzt die Rechte
// unabhängig von der umask.
func writeNew(path string, perm os.FileMode, block *pem.Block) error {
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, perm)
if err != nil {
return err
}
err = pem.Encode(f, block)
if cerr := f.Close(); err == nil {
err = cerr
}
if err == nil {
err = os.Chmod(path, perm)
}
if err != nil {
os.Remove(path)
}
return err
}
// ServerTLSConfig liefert die TLS-Konfiguration für den Agent-Port (:8443):
// nur TLS 1.3.
func ServerTLSConfig(sc ServerCert) *tls.Config {
return &tls.Config{
Certificates: []tls.Certificate{sc.Certificate},
MinVersion: tls.VersionTLS13,
}
}