// 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_.[.]. // 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, } }