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
}
+474
View File
@@ -0,0 +1,474 @@
package gatewaytls
import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
"encoding/pem"
"math/big"
"net"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
const (
testGatewayID = "7d0d1308-5e50-45ef-984c-4508ca899f5a"
testCredentialID = "ba4d93f8-d692-4210-97db-5a561bfb32d3"
)
type testCA struct {
certificate *x509.Certificate
key *ecdsa.PrivateKey
der []byte
}
func TestLoadSeparatesTrustPoolsAndDerivesGatewayIdentity(t *testing.T) {
streamingCA := newTestCA(t, "streaming-ca")
controlCA := newTestCA(t, "control-ca")
identityCA := newTestCA(t, "identity-ca")
uri := mustURL(t, "spiffe://versevdi/gateway/"+testGatewayID+"/credential/"+testCredentialID)
leafDER, key := newLeaf(t, identityCA, "gateway.example", []*url.URL{uri}, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth})
certFile, keyFile := writeKeyPair(t, leafDER, identityCA.der, key)
streamingFile := writeCertificate(t, "streaming-ca.pem", streamingCA.der)
controlFile := writeCertificate(t, "control-ca.pem", controlCA.der)
material, err := Load(certFile, keyFile, streamingFile, controlFile, testGatewayID, "gateway.example")
if err != nil {
t.Fatalf("Load() error = %v", err)
}
digest := sha256.Sum256(leafDER)
if material.CertificateIdentity != "sha256:"+hex.EncodeToString(digest[:]) {
t.Fatalf("certificate identity = %q", material.CertificateIdentity)
}
if material.ServerTLS.MinVersion != tls.VersionTLS13 || material.ControlTLS.MinVersion != tls.VersionTLS13 || material.ServerTLS.ClientAuth != tls.RequireAndVerifyClientCert ||
material.ServerTLS.ClientCAs == nil || material.ControlTLS.RootCAs == nil ||
len(material.ServerTLS.Certificates) != 1 || len(material.ControlTLS.Certificates) != 1 {
t.Fatalf("TLS material = server:%#v control:%#v", material.ServerTLS, material.ControlTLS)
}
if got := material.ServerTLS.ClientCAs.Subjects(); len(got) != 1 || string(got[0]) != string(streamingCA.certificate.RawSubject) {
t.Fatalf("streaming ClientCAs = %x", got)
}
if got := material.ControlTLS.RootCAs.Subjects(); len(got) != 1 || string(got[0]) != string(controlCA.certificate.RawSubject) {
t.Fatalf("control RootCAs = %x", got)
}
if string(material.ServerTLS.Certificates[0].Certificate[0]) != string(material.ControlTLS.Certificates[0].Certificate[0]) {
t.Fatal("server and control TLS did not use the same gateway leaf")
}
trustedControlLeaf, _ := newLeaf(t, controlCA, "control.example", nil, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth})
parsedTrusted, _ := x509.ParseCertificate(trustedControlLeaf)
if _, err := parsedTrusted.Verify(x509.VerifyOptions{Roots: material.ControlTLS.RootCAs, DNSName: "control.example"}); err != nil {
t.Fatalf("control CA did not verify control server: %v", err)
}
wrongControlLeaf, _ := newLeaf(t, streamingCA, "control.example", nil, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth})
parsedWrong, _ := x509.ParseCertificate(wrongControlLeaf)
if _, err := parsedWrong.Verify(x509.VerifyOptions{Roots: material.ControlTLS.RootCAs, DNSName: "control.example"}); err == nil {
t.Fatal("control RootCAs trusted the streaming CA")
}
}
func TestLoadTrustsExactSelfSignedControlLeaf(t *testing.T) {
streamingCA := newTestCA(t, "streaming-ca")
identityCA := newTestCA(t, "identity-ca")
uri := mustURL(t, "spiffe://versevdi/gateway/"+testGatewayID+"/credential/"+testCredentialID)
gatewayLeaf, gatewayKey := newLeaf(t, identityCA, "gateway.example", []*url.URL{uri}, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth})
certFile, keyFile := writeKeyPair(t, gatewayLeaf, identityCA.der, gatewayKey)
streamingFile := writeCertificate(t, "streaming-ca.pem", streamingCA.der)
controlLeaf, controlKey := newSelfSignedServerLeaf(t, net.ParseIP("127.0.0.1"))
controlFile := writeCertificate(t, "control-leaf.pem", controlLeaf)
material, err := Load(certFile, keyFile, streamingFile, controlFile, testGatewayID, "gateway.example")
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if material.ControlTLS.MinVersion != tls.VersionTLS13 {
t.Fatalf("ControlTLS.MinVersion = %d, want TLS 1.3", material.ControlTLS.MinVersion)
}
clientCAs := x509.NewCertPool()
clientCAs.AddCert(identityCA.certificate)
trusted := newControlServer(t, controlLeaf, controlKey, clientCAs)
defer trusted.Close()
response, err := (&http.Client{Transport: &http.Transport{TLSClientConfig: material.ControlTLS}}).Get(trusted.URL)
if err != nil {
t.Fatalf("pinned self-signed control leaf handshake: %v", err)
}
response.Body.Close()
if response.StatusCode != http.StatusNoContent {
t.Fatalf("trusted control response status = %d", response.StatusCode)
}
wrongLeaf, wrongKey := newSelfSignedServerLeaf(t, net.ParseIP("127.0.0.1"))
wrong := newControlServer(t, wrongLeaf, wrongKey, clientCAs)
defer wrong.Close()
if _, err := (&http.Client{Transport: &http.Transport{TLSClientConfig: material.ControlTLS}}).Get(wrong.URL); err == nil {
t.Fatal("different self-signed leaf with the same SAN completed handshake")
}
mismatchedLeaf, mismatchedKey := newSelfSignedServerLeaf(t, net.ParseIP("127.0.0.2"))
mismatchedFile := writeCertificate(t, "mismatched-control-leaf.pem", mismatchedLeaf)
mismatchedMaterial, err := Load(certFile, keyFile, streamingFile, mismatchedFile, testGatewayID, "gateway.example")
if err != nil {
t.Fatalf("Load() mismatched control leaf error = %v", err)
}
mismatched := newControlServer(t, mismatchedLeaf, mismatchedKey, clientCAs)
defer mismatched.Close()
if _, err := (&http.Client{Transport: &http.Transport{TLSClientConfig: mismatchedMaterial.ControlTLS}}).Get(mismatched.URL); err == nil {
t.Fatal("control leaf without the request hostname/SAN completed handshake")
}
if _, err := Load(certFile, keyFile, controlFile, controlFile, testGatewayID, "gateway.example"); err == nil {
t.Fatal("Load() accepted a non-CA leaf as streaming trust")
}
}
func TestLoadTrustsExactCAIssuedControlLeaf(t *testing.T) {
streamingCA := newTestCA(t, "streaming-ca")
controlCA := newTestCA(t, "control-ca")
identityCA := newTestCA(t, "identity-ca")
uri := mustURL(t, "spiffe://versevdi/gateway/"+testGatewayID+"/credential/"+testCredentialID)
gatewayLeaf, gatewayKey := newLeaf(t, identityCA, "gateway.example", []*url.URL{uri}, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth})
certFile, keyFile := writeKeyPair(t, gatewayLeaf, identityCA.der, gatewayKey)
streamingFile := writeCertificate(t, "streaming-ca.pem", streamingCA.der)
activeLeaf, activeKey := newCAIssuedControlLeaf(t, controlCA, net.ParseIP("127.0.0.1"))
controlFile := writeCertificate(t, "control-leaf.pem", activeLeaf)
material, err := Load(certFile, keyFile, streamingFile, controlFile, testGatewayID, "gateway.example")
if err != nil {
t.Fatalf("Load() error = %v", err)
}
clientCAs := x509.NewCertPool()
clientCAs.AddCert(identityCA.certificate)
trusted := newControlServer(t, activeLeaf, activeKey, clientCAs, controlCA.der)
defer trusted.Close()
response, err := (&http.Client{Transport: &http.Transport{TLSClientConfig: material.ControlTLS}}).Get(trusted.URL)
if err != nil {
t.Fatalf("pinned CA-issued control leaf handshake: %v", err)
}
response.Body.Close()
if response.TLS == nil || response.TLS.Version != tls.VersionTLS13 {
t.Fatalf("pinned control leaf TLS version = %v, want TLS 1.3", response.TLS)
}
if response.StatusCode != http.StatusNoContent {
t.Fatalf("trusted control response status = %d", response.StatusCode)
}
siblingLeaf, siblingKey := newCAIssuedControlLeaf(t, controlCA, net.ParseIP("127.0.0.1"))
sibling := newControlServer(t, siblingLeaf, siblingKey, clientCAs, controlCA.der)
defer sibling.Close()
if _, err := (&http.Client{Transport: &http.Transport{TLSClientConfig: material.ControlTLS}}).Get(sibling.URL); err == nil {
t.Fatal("sibling control leaf from the same issuer and SAN completed handshake")
}
}
func TestLoadRejectsControlLeafBundlesAndNonServerAuth(t *testing.T) {
streamingCA := newTestCA(t, "streaming-ca")
controlCA := newTestCA(t, "control-ca")
identityCA := newTestCA(t, "identity-ca")
uri := mustURL(t, "spiffe://versevdi/gateway/"+testGatewayID+"/credential/"+testCredentialID)
gatewayLeaf, gatewayKey := newLeaf(t, identityCA, "gateway.example", []*url.URL{uri}, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth})
certFile, keyFile := writeKeyPair(t, gatewayLeaf, identityCA.der, gatewayKey)
streamingFile := writeCertificate(t, "streaming-ca.pem", streamingCA.der)
serverLeaf, _ := newCAIssuedControlLeaf(t, controlCA, net.ParseIP("127.0.0.1"))
nonServerLeaf, _ := newLeaf(t, controlCA, "control.example", nil, []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth})
for _, test := range []struct {
name string
certificates [][]byte
}{
{"leaf and issuer chain", [][]byte{serverLeaf, controlCA.der}},
{"non ServerAuth leaf", [][]byte{nonServerLeaf}},
} {
t.Run(test.name, func(t *testing.T) {
controlFile := writeCertificateBundle(t, "control.pem", test.certificates...)
if _, err := Load(certFile, keyFile, streamingFile, controlFile, testGatewayID, "gateway.example"); err == nil {
t.Fatal("Load() accepted invalid control trust material")
}
})
}
}
func TestLoadRejectsWrongPublicNameURIAndLeafUsage(t *testing.T) {
streamingCA := newTestCA(t, "streaming-ca")
controlCA := newTestCA(t, "control-ca")
identityCA := newTestCA(t, "identity-ca")
streamingFile := writeCertificate(t, "streaming-ca.pem", streamingCA.der)
controlFile := writeCertificate(t, "control-ca.pem", controlCA.der)
tests := []struct {
name string
uriGatewayID string
publicIdentity string
leafDNSName string
usages []x509.ExtKeyUsage
}{
{"wrong public name", testGatewayID, "other.example", "gateway.example", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}},
{"noncanonical public identity", testGatewayID, " gateway.example ", "gateway.example", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}},
{"wildcard is not an exact public identity", testGatewayID, "gateway.example", "*.example", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}},
{"wrong URI gateway", "c1f5d25a-47c6-44ed-b9be-e0e8e87d4ae2", "gateway.example", "gateway.example", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}},
{"missing client auth", testGatewayID, "gateway.example", "gateway.example", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
uri := mustURL(t, "spiffe://versevdi/gateway/"+test.uriGatewayID+"/credential/"+testCredentialID)
leafDER, key := newLeaf(t, identityCA, test.leafDNSName, []*url.URL{uri}, test.usages)
certFile, keyFile := writeKeyPair(t, leafDER, identityCA.der, key)
if _, err := Load(certFile, keyFile, streamingFile, controlFile, testGatewayID, test.publicIdentity); err == nil {
t.Fatal("Load() accepted invalid gateway certificate identity")
}
})
}
}
func TestLoadRejectsNonCanonicalPublicDNSIdentity(t *testing.T) {
streamingCA := newTestCA(t, "streaming-ca")
controlCA := newTestCA(t, "control-ca")
identityCA := newTestCA(t, "identity-ca")
streamingFile := writeCertificate(t, "streaming-ca.pem", streamingCA.der)
controlFile := writeCertificate(t, "control-ca.pem", controlCA.der)
uri := mustURL(t, "spiffe://versevdi/gateway/"+testGatewayID+"/credential/"+testCredentialID)
tooLongLabel := strings.Repeat("a", 64) + ".example"
tooLongName := strings.Repeat("a", 63) + "." + strings.Repeat("b", 63) + "." + strings.Repeat("c", 63) + "." + strings.Repeat("d", 62)
for _, test := range []struct {
name string
leafDNSName string
publicIdentity string
}{
{"uppercase exact SAN", "Gateway.example", "Gateway.example"},
{"empty label", "gateway..example", "gateway..example"},
{"leading hyphen", "-gateway.example", "-gateway.example"},
{"trailing hyphen", "gateway-.example", "gateway-.example"},
{"wildcard", "*.example", "*.example"},
{"label exceeds 63 bytes", tooLongLabel, tooLongLabel},
{"name exceeds 253 bytes", tooLongName, tooLongName},
{"IP address", "127.0.0.1", "127.0.0.1"},
{"port", "gateway.example:443", "gateway.example:443"},
{"path", "gateway.example/path", "gateway.example/path"},
{"whitespace", "gateway .example", "gateway .example"},
{"trailing dot", "gateway.example.", "gateway.example."},
{"Unicode", "gateway.example", "gäteway.example"},
} {
t.Run(test.name, func(t *testing.T) {
leafDER, key := newLeaf(t, identityCA, test.leafDNSName, []*url.URL{uri}, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth})
certFile, keyFile := writeKeyPair(t, leafDER, identityCA.der, key)
if _, err := Load(certFile, keyFile, streamingFile, controlFile, testGatewayID, test.publicIdentity); err == nil {
t.Fatal("Load() accepted a noncanonical public DNS identity")
}
})
}
}
func TestLoadAcceptsCanonicalPublicDNSIdentity(t *testing.T) {
streamingCA := newTestCA(t, "streaming-ca")
controlCA := newTestCA(t, "control-ca")
identityCA := newTestCA(t, "identity-ca")
streamingFile := writeCertificate(t, "streaming-ca.pem", streamingCA.der)
controlFile := writeCertificate(t, "control-ca.pem", controlCA.der)
uri := mustURL(t, "spiffe://versevdi/gateway/"+testGatewayID+"/credential/"+testCredentialID)
for _, publicIdentity := range []string{"gateway.example", "gateway"} {
t.Run(publicIdentity, func(t *testing.T) {
leafDER, key := newLeaf(t, identityCA, publicIdentity, []*url.URL{uri}, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth})
certFile, keyFile := writeKeyPair(t, leafDER, identityCA.der, key)
if _, err := Load(certFile, keyFile, streamingFile, controlFile, testGatewayID, publicIdentity); err != nil {
t.Fatalf("Load() error = %v", err)
}
})
}
}
func TestLoadRejectsLegacyAndNoncanonicalGatewayURIs(t *testing.T) {
streamingCA := newTestCA(t, "streaming-ca")
controlCA := newTestCA(t, "control-ca")
identityCA := newTestCA(t, "identity-ca")
streamingFile := writeCertificate(t, "streaming-ca.pem", streamingCA.der)
controlFile := writeCertificate(t, "control-ca.pem", controlCA.der)
canonical := "spiffe://versevdi/gateway/" + testGatewayID + "/credential/" + testCredentialID
for _, test := range []struct {
name string
uri string
}{
{"legacy URN", "urn:versevdi:gateway:" + testGatewayID + ":credential:" + testCredentialID},
{"opaque URI", "spiffe:gateway/" + testGatewayID + "/credential/" + testCredentialID},
{"leading path slash", "spiffe://versevdi//gateway/" + testGatewayID + "/credential/" + testCredentialID},
{"trailing path slash", canonical + "/"},
{"percent encoded credential", "spiffe://versevdi/gateway/" + testGatewayID + "/credential/%62a4d93f8-d692-4210-97db-5a561bfb32d3"},
{"force query", canonical + "?"},
{"query", canonical + "?version=1"},
{"fragment", canonical + "#fragment"},
{"user", "spiffe://gateway@versevdi/gateway/" + testGatewayID + "/credential/" + testCredentialID},
} {
t.Run(test.name, func(t *testing.T) {
uri := mustURL(t, test.uri)
leafDER, key := newLeaf(t, identityCA, "gateway.example", []*url.URL{uri}, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth})
certFile, keyFile := writeKeyPair(t, leafDER, identityCA.der, key)
if _, err := Load(certFile, keyFile, streamingFile, controlFile, testGatewayID, "gateway.example"); err == nil {
t.Fatal("Load() accepted a legacy or noncanonical gateway URI")
}
})
}
t.Run("uppercase UUIDs", func(t *testing.T) {
gatewayID := strings.ToUpper(testGatewayID)
uri := mustURL(t, "spiffe://versevdi/gateway/"+gatewayID+"/credential/"+strings.ToUpper(testCredentialID))
leafDER, key := newLeaf(t, identityCA, "gateway.example", []*url.URL{uri}, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth})
certFile, keyFile := writeKeyPair(t, leafDER, identityCA.der, key)
if _, err := Load(certFile, keyFile, streamingFile, controlFile, gatewayID, "gateway.example"); err == nil {
t.Fatal("Load() accepted noncanonical uppercase UUIDs")
}
})
}
func newTestCA(t *testing.T, commonName string) testCA {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
template := &x509.Certificate{
SerialNumber: big.NewInt(time.Now().UnixNano()), Subject: pkix.Name{CommonName: commonName},
NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour),
IsCA: true, BasicConstraintsValid: true, KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
}
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
if err != nil {
t.Fatal(err)
}
certificate, err := x509.ParseCertificate(der)
if err != nil {
t.Fatal(err)
}
return testCA{certificate: certificate, key: key, der: der}
}
func newLeaf(t *testing.T, ca testCA, dnsName string, uris []*url.URL, usages []x509.ExtKeyUsage) ([]byte, ed25519.PrivateKey) {
t.Helper()
publicKey, key, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
template := &x509.Certificate{
SerialNumber: big.NewInt(time.Now().UnixNano()), Subject: pkix.Name{CommonName: dnsName}, DNSNames: []string{dnsName}, URIs: uris,
NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour),
BasicConstraintsValid: true, KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: usages,
}
der, err := x509.CreateCertificate(rand.Reader, template, ca.certificate, publicKey, ca.key)
if err != nil {
t.Fatal(err)
}
return der, key
}
func newSelfSignedServerLeaf(t *testing.T, ip net.IP) ([]byte, *ecdsa.PrivateKey) {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
template := &x509.Certificate{
SerialNumber: big.NewInt(time.Now().UnixNano()), Subject: pkix.Name{CommonName: "control.example"}, IPAddresses: []net.IP{ip},
NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour),
BasicConstraintsValid: true, KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
if err != nil {
t.Fatal(err)
}
return der, key
}
func newCAIssuedControlLeaf(t *testing.T, ca testCA, ip net.IP) ([]byte, ed25519.PrivateKey) {
t.Helper()
publicKey, key, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
template := &x509.Certificate{
SerialNumber: big.NewInt(time.Now().UnixNano()), Subject: pkix.Name{CommonName: "control.example"}, IPAddresses: []net.IP{ip},
NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour),
BasicConstraintsValid: true, KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
der, err := x509.CreateCertificate(rand.Reader, template, ca.certificate, publicKey, ca.key)
if err != nil {
t.Fatal(err)
}
return der, key
}
func newControlServer(t *testing.T, leaf []byte, key any, clientCAs *x509.CertPool, chain ...[]byte) *httptest.Server {
t.Helper()
server := httptest.NewUnstartedServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
if request.TLS == nil || len(request.TLS.PeerCertificates) == 0 {
http.Error(response, "mTLS required", http.StatusUnauthorized)
return
}
response.WriteHeader(http.StatusNoContent)
}))
certificates := append([][]byte{leaf}, chain...)
server.TLS = &tls.Config{
MinVersion: tls.VersionTLS13, Certificates: []tls.Certificate{{Certificate: certificates, PrivateKey: key}},
ClientAuth: tls.RequireAndVerifyClientCert, ClientCAs: clientCAs,
}
server.StartTLS()
return server
}
func writeKeyPair(t *testing.T, leafDER, issuerDER []byte, key ed25519.PrivateKey) (string, string) {
t.Helper()
directory := t.TempDir()
certFile := filepath.Join(directory, "gateway.pem")
keyFile := filepath.Join(directory, "gateway-key.pem")
certificatePEM := append(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafDER}), pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: issuerDER})...)
privateDER, err := x509.MarshalPKCS8PrivateKey(key)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(certFile, certificatePEM, 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(keyFile, pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privateDER}), 0o600); err != nil {
t.Fatal(err)
}
return certFile, keyFile
}
func writeCertificate(t *testing.T, name string, der []byte) string {
return writeCertificateBundle(t, name, der)
}
func writeCertificateBundle(t *testing.T, name string, certificates ...[]byte) string {
t.Helper()
path := filepath.Join(t.TempDir(), name)
var encoded []byte
for _, certificate := range certificates {
encoded = append(encoded, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certificate})...)
}
if err := os.WriteFile(path, encoded, 0o600); err != nil {
t.Fatal(err)
}
return path
}
func mustURL(t *testing.T, value string) *url.URL {
t.Helper()
parsed, err := url.Parse(value)
if err != nil {
t.Fatal(err)
}
return parsed
}
+11 -29
View File
@@ -2,14 +2,11 @@ package main
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"flag"
"fmt"
"log"
"net"
"os"
"os/signal"
"strings"
"syscall"
@@ -17,6 +14,7 @@ import (
"net/http"
"git.sechmachine.io.vn/sechmachine/VerseVDI-Data-Plane/cmd/internal/gatewaytls"
"git.sechmachine.io.vn/sechmachine/VerseVDI-Data-Plane/gateway"
protocol "git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol/gen/go/protocol"
)
@@ -28,20 +26,20 @@ func main() {
}
func run() error {
var listen, advertiseAddress, controlPlane, certFile, keyFile, clientCAFile string
var gatewayID, instanceIdentity, certificateIdentity, publicIdentity string
var listen, advertiseAddress, controlPlane, certFile, keyFile, streamingCAFile, controlCAFile string
var gatewayID, instanceIdentity, publicIdentity string
flag.StringVar(&listen, "listen", "0.0.0.0:443", "gateway QUIC listen address")
flag.StringVar(&advertiseAddress, "advertise-address", "", "client-visible gateway address host:port")
flag.StringVar(&controlPlane, "control-plane", "", "Connection Server HTTPS base URL")
flag.StringVar(&certFile, "cert", "", "gateway certificate PEM")
flag.StringVar(&keyFile, "key", "", "gateway private key PEM")
flag.StringVar(&clientCAFile, "client-ca", "", "Connection Server/client CA PEM")
flag.StringVar(&streamingCAFile, "streaming-ca", "", "CA PEM for QUIC streaming clients")
flag.StringVar(&controlCAFile, "control-ca", "", "CA bundle or exact Server leaf PEM")
flag.StringVar(&gatewayID, "gateway-id", "", "stable gateway identifier")
flag.StringVar(&instanceIdentity, "instance-identity", "", "gateway instance identity")
flag.StringVar(&certificateIdentity, "certificate-identity", "", "gateway certificate identity")
flag.StringVar(&publicIdentity, "public-identity", "gateway", "gateway public identity")
flag.StringVar(&publicIdentity, "public-identity", "", "gateway DNS identity from the certificate SAN")
flag.Parse()
for name, value := range map[string]string{"control-plane": controlPlane, "advertise-address": advertiseAddress, "cert": certFile, "key": keyFile, "client-ca": clientCAFile, "gateway-id": gatewayID, "instance-identity": instanceIdentity, "certificate-identity": certificateIdentity} {
for name, value := range map[string]string{"control-plane": controlPlane, "advertise-address": advertiseAddress, "cert": certFile, "key": keyFile, "streaming-ca": streamingCAFile, "control-ca": controlCAFile, "gateway-id": gatewayID, "instance-identity": instanceIdentity, "public-identity": publicIdentity} {
if value == "" {
return fmt.Errorf("-%s is required", name)
}
@@ -49,20 +47,20 @@ func run() error {
if err := validateAdvertisedAddress(advertiseAddress); err != nil {
return err
}
serverTLS, clientTLS, err := loadTLS(certFile, keyFile, clientCAFile)
tlsMaterial, err := gatewaytls.Load(certFile, keyFile, streamingCAFile, controlCAFile, gatewayID, publicIdentity)
if err != nil {
return err
}
transport := &http.Transport{TLSClientConfig: clientTLS}
transport := &http.Transport{TLSClientConfig: tlsMaterial.ControlTLS}
controlPlaneClient := gateway.NewControlPlaneClient(controlPlane, &http.Client{Transport: transport, Timeout: 5 * time.Second})
provider := gateway.NewApolloAdapter(gateway.NewNativeApolloBackend(), gateway.ProviderIdentity{})
capabilities := gateway.DefaultCapabilities()
features := gateway.DefaultFeatures()
server, err := gateway.NewServer(gateway.ServerConfig{ListenAddress: listen, TLSConfig: serverTLS, GatewayID: gatewayID, Features: features, Capabilities: capabilities, ProviderCapabilities: capabilities, Admission: controlPlaneClient, ProviderStateReporter: controlPlaneClient, ClipboardAuditReporter: controlPlaneClient, Provider: provider, PacerKbps: 100000})
server, err := gateway.NewServer(gateway.ServerConfig{ListenAddress: listen, TLSConfig: tlsMaterial.ServerTLS, GatewayID: gatewayID, Features: features, Capabilities: capabilities, ProviderCapabilities: capabilities, Admission: controlPlaneClient, ProviderStateReporter: controlPlaneClient, ClipboardAuditReporter: controlPlaneClient, Provider: provider, PacerKbps: 100000})
if err != nil {
return err
}
registration := protocol.GatewayRegistration{Version: "1", GatewayID: gatewayID, InstanceIdentity: instanceIdentity, CertificateIdentity: certificateIdentity, PublicIdentity: publicIdentity, Address: advertiseAddress, ProviderIdentity: "server-derived", ProtocolMinVersion: 1, ProtocolMaxVersion: 1, ConnectionCapacity: 8, BandwidthCapacityKbps: 100000, Features: features, Capabilities: capabilities}
registration := protocol.GatewayRegistration{Version: "1", GatewayID: gatewayID, InstanceIdentity: instanceIdentity, CertificateIdentity: tlsMaterial.CertificateIdentity, PublicIdentity: publicIdentity, Address: advertiseAddress, ProviderIdentity: "server-derived", ProtocolMinVersion: 1, ProtocolMaxVersion: 1, ConnectionCapacity: 8, BandwidthCapacityKbps: 100000, Features: features, Capabilities: capabilities}
if _, err := controlPlaneClient.Register(context.Background(), registration); err != nil {
_ = server.Close()
return err
@@ -183,22 +181,6 @@ func providerStateName(value uint64) string {
}
}
func loadTLS(certFile, keyFile, clientCAFile string) (*tls.Config, *tls.Config, error) {
certificate, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, nil, err
}
caBytes, err := os.ReadFile(clientCAFile)
if err != nil {
return nil, nil, err
}
clientCAs := x509.NewCertPool()
if !clientCAs.AppendCertsFromPEM(caBytes) {
return nil, nil, errors.New("client CA PEM contains no certificate")
}
return &tls.Config{MinVersion: tls.VersionTLS13, Certificates: []tls.Certificate{certificate}, ClientAuth: tls.RequireAndVerifyClientCert, ClientCAs: clientCAs}, &tls.Config{MinVersion: tls.VersionTLS13, Certificates: []tls.Certificate{certificate}, RootCAs: clientCAs}, nil
}
func parseProviderIdentity(value string) (gateway.ProviderIdentity, error) {
uniqueID, fingerprint, ok := strings.Cut(value, "#")
if !ok || uniqueID == "" || fingerprint == "" {
+1 -1
View File
@@ -3,7 +3,7 @@ module git.sechmachine.io.vn/sechmachine/VerseVDI-Data-Plane
go 1.26.5
require (
git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol v1.0.0-phase3d-macos-rc.1
git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol v1.0.0-phase3d-macos-rc.2
github.com/quic-go/quic-go v0.61.0
)
+2
View File
@@ -16,6 +16,8 @@ git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol v1.0.0-phase3c-gateway-rc.10
git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol v1.0.0-phase3c-gateway-rc.10/go.mod h1:7PhFIDhjtr20btWoEb2GqB+7dBpzJt43olrnHVutWoc=
git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol v1.0.0-phase3d-macos-rc.1 h1:uCH+etSQtzqfhhRP3dWdp6aIDTU55G7r1UEOAJfWsGw=
git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol v1.0.0-phase3d-macos-rc.1/go.mod h1:7PhFIDhjtr20btWoEb2GqB+7dBpzJt43olrnHVutWoc=
git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol v1.0.0-phase3d-macos-rc.2 h1:UdkgLXQ2fWFCTGcMblZHXhvQbDx2wfTy4eHyEvav72U=
git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol v1.0.0-phase3d-macos-rc.2/go.mod h1:7PhFIDhjtr20btWoEb2GqB+7dBpzJt43olrnHVutWoc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=