207 lines
5.9 KiB
Go
207 lines
5.9 KiB
Go
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
|
|
}
|