197 lines
5.2 KiB
Go
197 lines
5.2 KiB
Go
// Package popmtls provides mTLS configuration for Clavitor POPs.
|
|
//
|
|
// This package is used by the POP runtime to configure mutual TLS for
|
|
// POP-to-POP communication. It uses only Go stdlib (crypto/tls, crypto/x509).
|
|
//
|
|
// Usage:
|
|
//
|
|
// config, err := popmtls.LoadConfig("/opt/clavitor/certs", "use1")
|
|
// if err != nil {
|
|
// log.Fatal(err)
|
|
// }
|
|
//
|
|
// // As server
|
|
// listener, err := tls.Listen("tcp", ":8443", config)
|
|
//
|
|
// // As client
|
|
// conn, err := tls.Dial("tcp", "sg1.clavitor.ai:8443", config)
|
|
package popmtls
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
// Config holds TLS configuration for a POP.
|
|
type Config struct {
|
|
Region string
|
|
CertDir string
|
|
CertPath string
|
|
KeyPath string
|
|
ChainPath string
|
|
}
|
|
|
|
// LoadConfig loads POP certificate and CA chain from the specified directory.
|
|
//
|
|
// Parameters:
|
|
// - certDir: Directory containing <region>.crt, <region>.key, and ca-chain.crt
|
|
// - region: The POP region identifier (e.g., "use1", "sg1")
|
|
//
|
|
// Returns a tls.Config configured for mutual TLS with:
|
|
// - Client certificate for authentication
|
|
// - CA pool from ca-chain.crt for peer verification
|
|
// - RequireAndVerifyClientCert for mandatory mTLS
|
|
func LoadConfig(certDir, region string) (*tls.Config, error) {
|
|
certPath := filepath.Join(certDir, fmt.Sprintf("%s.crt", region))
|
|
keyPath := filepath.Join(certDir, fmt.Sprintf("%s.key", region))
|
|
chainPath := filepath.Join(certDir, "ca-chain.crt")
|
|
|
|
// Load POP certificate and key
|
|
certPEM, err := os.ReadFile(certPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read POP cert from %s: %w", certPath, err)
|
|
}
|
|
|
|
keyPEM, err := os.ReadFile(keyPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read POP key from %s: %w", keyPath, err)
|
|
}
|
|
|
|
// Load certificate (includes chain if present)
|
|
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load X509 key pair: %w", err)
|
|
}
|
|
|
|
// Load CA chain for peer verification
|
|
caPEM, err := os.ReadFile(chainPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read CA chain from %s: %w", chainPath, err)
|
|
}
|
|
|
|
caPool := x509.NewCertPool()
|
|
if !caPool.AppendCertsFromPEM(caPEM) {
|
|
return nil, fmt.Errorf("failed to parse CA chain from %s", chainPath)
|
|
}
|
|
|
|
return &tls.Config{
|
|
Certificates: []tls.Certificate{cert},
|
|
RootCAs: caPool,
|
|
ClientCAs: caPool,
|
|
ClientAuth: tls.RequireAndVerifyClientCert,
|
|
MinVersion: tls.VersionTLS13,
|
|
CipherSuites: nil, // Let Go choose secure defaults for TLS 1.3
|
|
}, nil
|
|
}
|
|
|
|
// LoadConfigWithRootFallback loads mTLS config, falling back to just root CA if intermediate is missing.
|
|
//
|
|
// This is useful during CA rotation or when the intermediate cert might be rotated separately.
|
|
func LoadConfigWithRootFallback(certDir, region string) (*tls.Config, error) {
|
|
// Try standard config first
|
|
config, err := LoadConfig(certDir, region)
|
|
if err == nil {
|
|
return config, nil
|
|
}
|
|
|
|
// If chain is missing, try loading just root CA
|
|
certPath := filepath.Join(certDir, fmt.Sprintf("%s.crt", region))
|
|
keyPath := filepath.Join(certDir, fmt.Sprintf("%s.key", region))
|
|
rootPath := filepath.Join(certDir, "root-ca.crt")
|
|
|
|
certPEM, err1 := os.ReadFile(certPath)
|
|
if err1 != nil {
|
|
return nil, fmt.Errorf("read POP cert: %w", err1)
|
|
}
|
|
|
|
keyPEM, err1 := os.ReadFile(keyPath)
|
|
if err1 != nil {
|
|
return nil, fmt.Errorf("read POP key: %w", err1)
|
|
}
|
|
|
|
rootPEM, err1 := os.ReadFile(rootPath)
|
|
if err1 != nil {
|
|
return nil, fmt.Errorf("read root CA (fallback failed): %w", err1)
|
|
}
|
|
|
|
cert, err1 := tls.X509KeyPair(certPEM, keyPEM)
|
|
if err1 != nil {
|
|
return nil, fmt.Errorf("load key pair: %w", err1)
|
|
}
|
|
|
|
caPool := x509.NewCertPool()
|
|
if !caPool.AppendCertsFromPEM(rootPEM) {
|
|
return nil, fmt.Errorf("parse root CA")
|
|
}
|
|
|
|
return &tls.Config{
|
|
Certificates: []tls.Certificate{cert},
|
|
RootCAs: caPool,
|
|
ClientCAs: caPool,
|
|
ClientAuth: tls.RequireAndVerifyClientCert,
|
|
MinVersion: tls.VersionTLS13,
|
|
}, nil
|
|
}
|
|
|
|
// ParseCertPool parses a PEM-encoded certificate bundle into a CertPool.
|
|
//
|
|
// This is useful when you need to manually construct a CA pool from
|
|
// embedded certificates or other sources.
|
|
func ParseCertPool(pemData []byte) (*x509.CertPool, error) {
|
|
pool := x509.NewCertPool()
|
|
if !pool.AppendCertsFromPEM(pemData) {
|
|
return nil, fmt.Errorf("no valid certificates found")
|
|
}
|
|
return pool, nil
|
|
}
|
|
|
|
// ExtractCertsFromPEM parses PEM data and returns individual DER-encoded certificates.
|
|
func ExtractCertsFromPEM(pemData []byte) ([][]byte, error) {
|
|
var certs [][]byte
|
|
data := pemData
|
|
|
|
for {
|
|
block, rest := pem.Decode(data)
|
|
if block == nil {
|
|
break
|
|
}
|
|
if block.Type == "CERTIFICATE" {
|
|
certs = append(certs, block.Bytes)
|
|
}
|
|
data = rest
|
|
if len(data) == 0 {
|
|
break
|
|
}
|
|
}
|
|
|
|
if len(certs) == 0 {
|
|
return nil, fmt.Errorf("no certificates found in PEM data")
|
|
}
|
|
|
|
return certs, nil
|
|
}
|
|
|
|
// BuildChain builds a certificate pool from a list of PEM files.
|
|
//
|
|
// Each file can contain one or more certificates. All certificates
|
|
// are appended to a single pool.
|
|
func BuildChain(paths ...string) (*x509.CertPool, error) {
|
|
pool := x509.NewCertPool()
|
|
|
|
for _, path := range paths {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read %s: %w", path, err)
|
|
}
|
|
if !pool.AppendCertsFromPEM(data) {
|
|
return nil, fmt.Errorf("parse certificates from %s", path)
|
|
}
|
|
}
|
|
|
|
return pool, nil
|
|
}
|