package gatewaytls import ( "bytes" "crypto/sha256" "crypto/tls" "crypto/x509" "encoding/hex" "encoding/pem" "errors" "fmt" "net" "net/url" "os" "slices" "strings" "time" ) type Material struct { ServerTLS *tls.Config ControlTLS *tls.Config CertificateIdentity string } func Load(certFile, keyFile, streamingCAFile, controlCAFile, gatewayID, publicIdentity string) (Material, error) { certificate, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil { return Material{}, fmt.Errorf("load gateway certificate: %w", err) } if len(certificate.Certificate) == 0 { return Material{}, errors.New("gateway certificate chain is empty") } leaf, err := x509.ParseCertificate(certificate.Certificate[0]) if err != nil { return Material{}, errors.New("parse gateway leaf certificate") } certificate.Leaf = leaf if now := time.Now(); now.Before(leaf.NotBefore) || now.After(leaf.NotAfter) { return Material{}, errors.New("gateway leaf certificate is not currently valid") } if !hasUsage(leaf, x509.ExtKeyUsageServerAuth) || !hasUsage(leaf, x509.ExtKeyUsageClientAuth) { return Material{}, errors.New("gateway leaf certificate requires ServerAuth and ClientAuth") } if !gatewayURIAllowed(leaf.URIs, gatewayID) { return Material{}, errors.New("gateway leaf certificate URI identity mismatch") } if !validPublicDNSName(publicIdentity) || !slices.Contains(leaf.DNSNames, publicIdentity) || leaf.VerifyHostname(publicIdentity) != nil { return Material{}, errors.New("gateway public identity must match a DNS SAN") } streamingCAs, err := loadCAPool(streamingCAFile) if err != nil { return Material{}, fmt.Errorf("load streaming CA: %w", err) } controlRoots, err := loadControlRootPool(controlCAFile) if err != nil { return Material{}, fmt.Errorf("load control root: %w", err) } digest := sha256.Sum256(leaf.Raw) return Material{ ServerTLS: &tls.Config{ MinVersion: tls.VersionTLS13, Certificates: []tls.Certificate{certificate}, ClientAuth: tls.RequireAndVerifyClientCert, ClientCAs: streamingCAs, }, ControlTLS: &tls.Config{ MinVersion: tls.VersionTLS13, Certificates: []tls.Certificate{certificate}, RootCAs: controlRoots, }, CertificateIdentity: "sha256:" + hex.EncodeToString(digest[:]), }, nil } func loadCAPool(path string) (*x509.CertPool, error) { encoded, err := os.ReadFile(path) if err != nil { return nil, err } pool := x509.NewCertPool() count := 0 for len(bytes.TrimSpace(encoded)) > 0 { block, rest := pem.Decode(encoded) if block == nil { return nil, errors.New("CA PEM contains invalid data") } encoded = rest if block.Type != "CERTIFICATE" { return nil, errors.New("CA PEM contains a non-certificate block") } certificate, parseErr := x509.ParseCertificate(block.Bytes) if parseErr != nil || !certificate.IsCA || certificate.KeyUsage&x509.KeyUsageCertSign == 0 { return nil, errors.New("CA PEM contains a non-CA certificate") } pool.AddCert(certificate) count++ } if count == 0 { return nil, errors.New("CA PEM contains no certificate") } return pool, nil } func loadControlRootPool(path string) (*x509.CertPool, error) { encoded, err := os.ReadFile(path) if err != nil { return nil, err } var certificates []*x509.Certificate for len(bytes.TrimSpace(encoded)) > 0 { block, rest := pem.Decode(encoded) if block == nil { return nil, errors.New("control trust PEM contains invalid data") } encoded = rest if block.Type != "CERTIFICATE" { return nil, errors.New("control trust PEM contains a non-certificate block") } certificate, parseErr := x509.ParseCertificate(block.Bytes) if parseErr != nil { return nil, errors.New("control trust PEM contains an invalid certificate") } certificates = append(certificates, certificate) } if len(certificates) == 0 { return nil, errors.New("control trust PEM contains no certificate") } pool := x509.NewCertPool() allCAs := true for _, certificate := range certificates { if !certificate.IsCA || certificate.KeyUsage&x509.KeyUsageCertSign == 0 { allCAs = false break } } if allCAs { for _, certificate := range certificates { pool.AddCert(certificate) } return pool, nil } leaf := certificates[0] if len(certificates) != 1 || leaf.IsCA || !hasUsage(leaf, x509.ExtKeyUsageServerAuth) { return nil, errors.New("control trust PEM must contain CAs or one ServerAuth leaf") } pool.AddCert(leaf) return pool, nil } func hasUsage(certificate *x509.Certificate, wanted x509.ExtKeyUsage) bool { for _, usage := range certificate.ExtKeyUsage { if usage == wanted { return true } } return false } func gatewayURIAllowed(uris []*url.URL, gatewayID string) bool { if !validUUID(gatewayID) { return false } prefix := "/gateway/" + gatewayID + "/credential/" for _, uri := range uris { if uri == nil || uri.Scheme != "spiffe" || uri.Host != "versevdi" || uri.User != nil || uri.Opaque != "" || uri.RawPath != "" || uri.RawQuery != "" || uri.ForceQuery || uri.Fragment != "" || uri.RawFragment != "" || !strings.HasPrefix(uri.Path, prefix) { continue } if validUUID(strings.TrimPrefix(uri.Path, prefix)) { return true } } return false } func validPublicDNSName(name string) bool { if len(name) == 0 || len(name) > 253 || net.ParseIP(name) != nil { return false } for _, label := range strings.Split(name, ".") { if len(label) == 0 || len(label) > 63 || label[0] == '-' || label[len(label)-1] == '-' { return false } for _, character := range []byte(label) { if !(character >= 'a' && character <= 'z' || character >= '0' && character <= '9' || character == '-') { return false } } } return true } func validUUID(value string) bool { if len(value) != 36 || value != strings.ToLower(value) || value[8] != '-' || value[13] != '-' || value[18] != '-' || value[23] != '-' { return false } decoded, err := hex.DecodeString(strings.ReplaceAll(value, "-", "")) if err != nil { return false } for _, b := range decoded { if b != 0 { return true } } return false }