feat(gateway): enforce phase 3d tls trust
Verify Data Plane / gateway (push) Successful in 4m35s

This commit is contained in:
sechmachine
2026-08-11 20:15:15 +07:00
parent a1d68d33e5
commit e764b0d96d
5 changed files with 694 additions and 30 deletions
+206
View File
@@ -0,0 +1,206 @@
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
}