package pbs import ( "crypto/sha256" "crypto/tls" "crypto/x509" "encoding/hex" "fmt" "strings" ) // pinnedTLS builds a tls.Config that pins the PBS server's leaf cert by SHA-256 — the same // model as the PVE client (proxmox/tls.go). PBS serves a self-signed cert, so we disable the // default chain check but enforce an exact-cert match: a spoofed PBS presents a different // fingerprint and is rejected. fingerprint is hex with optional colons (the form in // /etc/pve/storage.cfg and the slice-5 durable_id). func pinnedTLS(fingerprint string) (*tls.Config, error) { want, err := normalizeFingerprint(fingerprint) if err != nil { return nil, err } return &tls.Config{ InsecureSkipVerify: true, //nolint:gosec // replaced by the exact-cert pin below VerifyPeerCertificate: func(rawCerts [][]byte, _ [][]*x509.Certificate) error { if len(rawCerts) == 0 { return fmt.Errorf("pbs: TLS pin: peer presented no certificate") } got := sha256.Sum256(rawCerts[0]) if hex.EncodeToString(got[:]) != want { return fmt.Errorf("pbs: TLS pin mismatch: server cert sha256 does not match configured fingerprint") } return nil }, }, nil } // normalizeFingerprint lowercases and strips colons/whitespace, validating a 64-char // (32-byte) hex SHA-256. func normalizeFingerprint(fp string) (string, error) { s := strings.ToLower(strings.NewReplacer(":", "", " ", "", "\t", "").Replace(fp)) if len(s) != 64 { return "", fmt.Errorf("pbs: fingerprint must be a SHA-256 (64 hex chars), got %d", len(s)) } if _, err := hex.DecodeString(s); err != nil { return "", fmt.Errorf("pbs: fingerprint is not valid hex: %w", err) } return s, nil }