This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user