package crypto import ( "bufio" "bytes" "crypto/tls" "errors" "fmt" "os" "strings" "sync" ) var ( // ErrUnknownServer: Kein Pin und kein erwarteter Fingerprint – ein // ungeprüfter Erstkontakt wird verweigert. ErrUnknownServer = errors.New("crypto: unbekannter Server – Fingerprint aus dem Enrollment-Token erforderlich") // ErrFingerprintMismatch: Das Server-Zertifikat passt nicht zum Pin bzw. // zum erwarteten Fingerprint (möglicher Man-in-the-Middle). ErrFingerprintMismatch = errors.New("crypto: Server-Fingerprint stimmt nicht – möglicher Man-in-the-Middle") // ErrCorruptKnownServers: Die known_servers-Datei ist nicht lesbar. ErrCorruptKnownServers = errors.New("crypto: known_servers beschädigt") ) // KnownServers ist die Pin-Datei des Agenten (analog zu SSH known_hosts). // Format: eine Zeile " ", # für Kommentare. Einträge // werden nur angehängt, nie stillschweigend geändert. type KnownServers struct { Path string mu sync.Mutex } // Lookup liefert den Pin für server. Eine fehlende Datei bedeutet: kein Pin. func (k *KnownServers) Lookup(server string) (Fingerprint, bool, error) { k.mu.Lock() defer k.mu.Unlock() return k.lookup(server) } func (k *KnownServers) lookup(server string) (Fingerprint, bool, error) { data, err := os.ReadFile(k.Path) if errors.Is(err, os.ErrNotExist) { return "", false, nil } if err != nil { return "", false, err } sc := bufio.NewScanner(bytes.NewReader(data)) for line := 1; sc.Scan(); line++ { text := strings.TrimSpace(sc.Text()) if text == "" || strings.HasPrefix(text, "#") { continue } fields := strings.Fields(text) if len(fields) != 2 { return "", false, fmt.Errorf("%w: %s Zeile %d", ErrCorruptKnownServers, k.Path, line) } fp, err := ParseFingerprint(fields[1]) if err != nil { return "", false, fmt.Errorf("%w: %s Zeile %d: %w", ErrCorruptKnownServers, k.Path, line, err) } if fields[0] == server { return fp, true, nil } } return "", false, sc.Err() } // Add pinnt fp für server. Ist bereits ein anderer Fingerprint gepinnt, // liefert Add ErrFingerprintMismatch und ändert nichts. func (k *KnownServers) Add(server string, fp Fingerprint) error { if len(strings.Fields(server)) != 1 || strings.TrimSpace(server) != server { return fmt.Errorf("crypto: ungültiger Servername %q", server) } if _, err := ParseFingerprint(string(fp)); err != nil { return err } k.mu.Lock() defer k.mu.Unlock() pinned, ok, err := k.lookup(server) if err != nil { return err } if ok { if pinned != fp { return fmt.Errorf("%w: %s ist auf %s gepinnt, nicht %s", ErrFingerprintMismatch, server, pinned, fp) } return nil } f, err := os.OpenFile(k.Path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o600) if err != nil { return err } _, err = fmt.Fprintf(f, "%s %s\n", server, fp) if cerr := f.Close(); err == nil { err = cerr } return err } // ClientTLSConfig liefert die TLS-Konfiguration des Agenten für server // (host:port aus der --server-URL). Statt einer CA prüft sie den // Fingerprint: // // - Pin vorhanden: Das Zertifikat muss dazu passen (expected wird ignoriert). // - Kein Pin, expected gesetzt (Fingerprint aus dem Enrollment-Token): // Das Zertifikat muss dazu passen und wird dann gepinnt. // - Kein Pin, kein expected: Verbindung wird verweigert (ErrUnknownServer). // // Die Prüfung läuft im TLS-Handshake, also bevor irgendein Request-Byte // (Token, Signatur, Body) gesendet wird. Nur TLS 1.3. func ClientTLSConfig(server string, ks *KnownServers, expected Fingerprint) *tls.Config { return &tls.Config{ MinVersion: tls.VersionTLS13, // Die CA-Prüfung entfällt bewusst; VerifyConnection ersetzt sie // durch die strengere Fingerprint-Prüfung. InsecureSkipVerify: true, VerifyConnection: func(cs tls.ConnectionState) error { if len(cs.PeerCertificates) == 0 { return fmt.Errorf("%w: kein Zertifikat", ErrFingerprintMismatch) } actual := FingerprintOf(cs.PeerCertificates[0]) pinned, ok, err := ks.Lookup(server) if err != nil { return err } if ok { if actual != pinned { return fmt.Errorf("%w: %s präsentiert %s, gepinnt ist %s", ErrFingerprintMismatch, server, actual, pinned) } return nil } if expected == "" { return fmt.Errorf("%w (%s präsentiert %s)", ErrUnknownServer, server, actual) } if actual != expected { return fmt.Errorf("%w: %s präsentiert %s, Token erwartet %s", ErrFingerprintMismatch, server, actual, expected) } return ks.Add(server, actual) }, } }