package security import ( "crypto/tls" "crypto/x509" "errors" "fmt" "os" ) // allowedSuites is the AEAD cipher allowlist required by D-015. We only // support TLS 1.3, so the Go cipher suite names below are TLS 1.3 cipher // suites. In Go 1.22+, the CipherSuites field still works for TLS 1.2 // negotiation, but with MinVersion=tls.VersionTLS13 only the TLS 1.3 // suites apply. // // We pin the three NIST/CHACHA AEAD suites: // - TLS_AES_256_GCM_SHA384 // - TLS_CHACHA20_POLY1305_SHA256 // - TLS_AES_128_GCM_SHA256 // // No TLS 1.2 fallback. No CBC modes. No NULL/integrity-only modes. var allowedSuites = []uint16{ tls.TLS_AES_256_GCM_SHA384, tls.TLS_CHACHA20_POLY1305_SHA256, tls.TLS_AES_128_GCM_SHA256, } // AllowedCipherSuites returns a copy of the cipher allowlist. Exposed for // tests and for callers that want to construct their own tls.Config with // the same policy. func AllowedCipherSuites() []uint16 { out := make([]uint16, len(allowedSuites)) copy(out, allowedSuites) return out } // loadKeyPair is a small helper: load cert + key from disk, return // tls.Certificate. Errors are wrapped with the path that failed. func loadKeyPair(certPath, keyPath string) (tls.Certificate, error) { if certPath == "" || keyPath == "" { return tls.Certificate{}, errors.New("loadKeyPair: certPath and keyPath are required") } cert, err := tls.LoadX509KeyPair(certPath, keyPath) if err != nil { return tls.Certificate{}, fmt.Errorf("load cert/key pair (%s, %s): %w", certPath, keyPath, err) } return cert, nil } // loadCAPool reads a PEM CA cert file and returns a CertPool containing // that cert. We use the subject as the trust anchor — clients verify // server certs against this single CA. func loadCAPool(caPath string) (*x509.CertPool, error) { if caPath == "" { return nil, errors.New("loadCAPool: caPath is required") } caPEM, err := os.ReadFile(caPath) if err != nil { return nil, fmt.Errorf("read CA cert: %w", err) } pool := x509.NewCertPool() if !pool.AppendCertsFromPEM(caPEM) { return nil, fmt.Errorf("parse CA cert PEM from %s", caPath) } return pool, nil } // ServerTLSConfig returns a *tls.Config suitable for an mTLS server. The // server presents certPath/keyPath and requires client certs signed by // the CA at caPath. The cipher allowlist + MinVersion=1.3 are enforced. // // ClientCAs is the same pool as the trust store — peers present certs // signed by the same CA, and we verify them. GetCertificate is left nil; // callers (the daemon) populate it to enable hot-swap on cert renewal. // // Returns an error if any path is missing or any file cannot be read. func ServerTLSConfig(certPath, keyPath, caPath string) (*tls.Config, error) { if _, err := loadKeyPair(certPath, keyPath); err != nil { return nil, err } pool, err := loadCAPool(caPath) if err != nil { return nil, err } return &tls.Config{ MinVersion: tls.VersionTLS13, MaxVersion: tls.VersionTLS13, CipherSuites: AllowedCipherSuites(), Certificates: []tls.Certificate{{Certificate: nil}}, // placeholder; daemon fills via GetCertificate ClientCAs: pool, ClientAuth: tls.RequireAndVerifyClientCert, NextProtos: []string{"h2", "http/1.1"}, }, nil } // ClientTLSConfig returns a *tls.Config suitable for an mTLS client. The // client verifies the server cert against the CA at caPath. If certPath // and keyPath are both non-empty, the client also presents a cert (for // mutual auth). If only one is set, the call fails — both-or-neither. // // serverName is the expected server identity (SNI / cert SAN match). It // MUST match a SAN on the server cert; the standard tls.Config will then // validate it during the handshake. For extra safety, callers should also // use VerifyPeerCertificate to enforce a pinned peer identity. func ClientTLSConfig(caPath, serverName string, certPath, keyPath string) (*tls.Config, error) { pool, err := loadCAPool(caPath) if err != nil { return nil, err } cfg := &tls.Config{ MinVersion: tls.VersionTLS13, MaxVersion: tls.VersionTLS13, CipherSuites: AllowedCipherSuites(), RootCAs: pool, ServerName: serverName, NextProtos: []string{"h2", "http/1.1"}, } hasCert, hasKey := certPath != "", keyPath != "" if hasCert != hasKey { return nil, errors.New("ClientTLSConfig: certPath and keyPath must be both set or both empty") } if hasCert && hasKey { cert, err := loadKeyPair(certPath, keyPath) if err != nil { return nil, err } cfg.Certificates = []tls.Certificate{cert} } return cfg, nil }