Skip to content

Commit

Permalink
WIP Issue 3138 - Conformance Tests for BackendTLSPolicy - normative
Browse files Browse the repository at this point in the history
  • Loading branch information
candita committed Jul 23, 2024
1 parent 9787352 commit 76c8e10
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 10 deletions.
79 changes: 69 additions & 10 deletions conformance/echo-basic/echo-basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@ import (
"encoding/pem"
"fmt"
"io"
"net"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"time"

"github.com/paultag/sniff/parser"

"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"golang.org/x/net/websocket"
Expand All @@ -48,15 +51,19 @@ type RequestAssertions struct {
Context `json:",inline"`

TLS *TLSAssertions `json:"tls,omitempty"`
SNI string `json:"sni"`
}

// TLSAssertions contains information about the TLS connection.
type TLSAssertions struct {
Version string `json:"version"`
PeerCertificates []string `json:"peerCertificates,omitempty"`
ServerName string `json:"serverName"`
NegotiatedProtocol string `json:"negotiatedProtocol,omitempty"`
CipherSuite string `json:"cipherSuite"`
Version string `json:"version"`
PeerCertificates []string `json:"peerCertificates,omitempty"`
// ClientCertificates are used by the gateway to authorize itself to the backend.
ClientCertificates []string `json:"clientCertificates,omitempty"`
// ServerName is the SNI.
ServerName string `json:"serverName"`
NegotiatedProtocol string `json:"negotiatedProtocol,omitempty"`
CipherSuite string `json:"cipherSuite"`
}

type preserveSlashes struct {
Expand Down Expand Up @@ -109,6 +116,7 @@ func main() {
httpMux.HandleFunc("/health", healthHandler)
httpMux.HandleFunc("/status/", statusHandler)
httpMux.HandleFunc("/", echoHandler)
httpMux.HandleFunc("/backendTLS", echoHandler)
httpMux.Handle("/ws", websocket.Handler(wsHandler))
httpHandler := &preserveSlashes{httpMux}

Expand All @@ -124,11 +132,14 @@ func main() {

go runH2CServer(h2cPort, errchan)

// Enable HTTPS if certificate and private key are given.
if os.Getenv("TLS_SERVER_CERT") != "" && os.Getenv("TLS_SERVER_PRIVKEY") != "" {
// Enable HTTPS if server certificate and private key are given. Enable secure backend if client certificate and key are given.
if os.Getenv("TLS_SERVER_CERT") != "" && os.Getenv("TLS_SERVER_PRIVKEY") != "" ||
os.Getenv("TLS_CLIENT_CERT") != "" && os.Getenv("TLS_CLIENT_KEY") != "" {
go func() {
fmt.Printf("Starting server, listening on port %s (https)\n", httpsPort)
err := listenAndServeTLS(fmt.Sprintf(":%s", httpsPort), os.Getenv("TLS_SERVER_CERT"), os.Getenv("TLS_SERVER_PRIVKEY"), os.Getenv("TLS_CLIENT_CACERTS"), httpHandler)
// TODO - probably don't need to pass these in.
err := listenAndServeTLS(fmt.Sprintf(":%s", httpsPort), os.Getenv("TLS_SERVER_CERT"), os.Getenv("TLS_SERVER_PRIVKEY"),
os.Getenv("TLS_CLIENT_CACERTS"), os.Getenv("TLS_CLIENT_CERT"), os.Getenv("TLS_CLIENT_KEY"), httpHandler)
if err != nil {
errchan <- err
}
Expand Down Expand Up @@ -201,15 +212,27 @@ func runH2CServer(h2cPort string, errchan chan<- error) {
}

func echoHandler(w http.ResponseWriter, r *http.Request) {
var sni string

fmt.Printf("Echoing back request made to %s to client (%s)\n", r.RequestURI, r.RemoteAddr)

// If the request has form ?delay=[:duration] wait for duration
// For example, ?delay=10s will cause the response to wait 10s before responding
if err := delayResponse(r); err != nil {
err := delayResponse(r)
if err != nil {
processError(w, err, http.StatusInternalServerError)
return
}

// If the request was made to URI backendTLS, then get the server name indication and
// add it to the RequestAssertions. It will be echoed back later.
if strings.Contains(r.RequestURI, "backendTLS") {
sni, err = sniffForSNI(r.RemoteAddr)
if err != nil {
// Todo: research if for some test cases there won't be one
}
}

requestAssertions := RequestAssertions{
r.RequestURI,
r.Host,
Expand All @@ -220,6 +243,7 @@ func echoHandler(w http.ResponseWriter, r *http.Request) {
context,

tlsStateToAssertions(r.TLS),
sni,
}

js, err := json.MarshalIndent(requestAssertions, "", " ")
Expand All @@ -232,6 +256,7 @@ func echoHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Content-Type-Options", "nosniff")
_, _ = w.Write(js)

}

func writeEchoResponseHeaders(w http.ResponseWriter, headers http.Header) {
Expand Down Expand Up @@ -267,7 +292,7 @@ func processError(w http.ResponseWriter, err error, code int) { //nolint:unparam
_, _ = w.Write(body)
}

func listenAndServeTLS(addr string, serverCert string, serverPrivKey string, clientCA string, handler http.Handler) error {
func listenAndServeTLS(addr string, serverCert string, serverPrivKey string, clientCA string, clientCert string, clientPrivKey string, handler http.Handler) error {
var config tls.Config

// Optionally enable client certificate validation when client CA certificates are given.
Expand Down Expand Up @@ -296,6 +321,40 @@ func listenAndServeTLS(addr string, serverCert string, serverPrivKey string, cli
return srv.ListenAndServeTLS(serverCert, serverPrivKey)
}

// sniffForSNI uses the request address to listen for the incoming TLS connection,
// and tries to find the server name indication from that connection.
func sniffForSNI(addr string) (string, error) {
var sni string

// Listen to get the SNI, and store in config.
listener, err := net.Listen("tcp", addr)
if err != nil {
return "", err
}
defer listener.Close()

for {
conn, err := listener.Accept()
if err != nil {
return "", err
}
data := make([]byte, 4096)
_, err = conn.Read(data)
if err != nil {
return "", fmt.Errorf("could not read socket: %v", err)
}
// Take an incoming TLS Client Hello and return the SNI name.
sni, err = parser.GetHostname(data[:])
if err != nil {
return "", fmt.Errorf("error getting SNI: %v", err)
}
if sni == "" {
return "", fmt.Errorf("no server name indication found")
}
return sni, nil
}
}

func tlsStateToAssertions(connectionState *tls.ConnectionState) *TLSAssertions {
if connectionState != nil {
var state TLSAssertions
Expand Down
76 changes: 76 additions & 0 deletions conformance/utils/kubernetes/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,79 @@ func generateRSACert(hosts []string, keyOut, certOut io.Writer) error {

return nil
}

// MustCreateCASignedCertSecret will create a secret using a CA Certificate, and public and private key for that certificate.
func MustCreateCASignedCertSecret(t *testing.T, namespace, secretName string, hosts []string) *corev1.Secret {
require.NotEmpty(t, hosts, "require a non-empty hosts for Subject Alternate Name values")

var serverKey, serverCert bytes.Buffer

require.NoError(t, generateCACert(hosts, &serverKey, &serverCert), "failed to generate CA certificate")

data := map[string][]byte{
corev1.TLSCertKey: serverCert.Bytes(),
corev1.TLSPrivateKeyKey: serverKey.Bytes(),
}

newSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: secretName,
},
Type: corev1.SecretTypeTLS,
Data: data,
}

return newSecret
}

// generateCACert generates a CA Certificate signed certificate valid for a year.
func generateCACert(hosts []string, keyOut, certOut io.Writer) error {
// Create the CA certificate.
ca := &x509.Certificate{
SerialNumber: big.NewInt(2024),
Subject: pkix.Name{
Organization: []string{"Kubernetes Gateway API"},
Country: []string{"US"},
Province: []string{""},
Locality: []string{"Boston"},
StreetAddress: []string{"Melnea Cass Blvd"},
PostalCode: []string{"02120"},
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1, 0, 0),
IsCA: true, // Indicates this is a CA Certificate.
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
}

for _, h := range hosts {
if ip := net.ParseIP(h); ip != nil {
ca.IPAddresses = append(ca.IPAddresses, ip)
} else {
ca.DNSNames = append(ca.DNSNames, h)
}
}

// Generate the private key.
caPrivKey, err := rsa.GenerateKey(rand.Reader, rsaBits)
if err != nil {
return err
}

// Generate the certificate using the CA certificate.
caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey)
if err != nil {
return err
}

if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: caBytes}); err != nil {
return fmt.Errorf("failed creating cert: %w", err)
}

if err := pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey)}); err != nil {
return fmt.Errorf("failed creating key: %w", err)
}
return nil
}
2 changes: 2 additions & 0 deletions conformance/utils/suite/suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,8 @@ func (suite *ConformanceTestSuite) Setup(t *testing.T, tests []ConformanceTest)
suite.Applier.MustApplyObjectsWithCleanup(t, suite.Client, suite.TimeoutConfig, []client.Object{secret}, suite.Cleanup)
secret = kubernetes.MustCreateSelfSignedCertSecret(t, "gateway-conformance-app-backend", "tls-passthrough-checks-certificate", []string{"abc.example.com"})
suite.Applier.MustApplyObjectsWithCleanup(t, suite.Client, suite.TimeoutConfig, []client.Object{secret}, suite.Cleanup)
caSecret := kubernetes.MustCreateCASignedCertSecret(t, "gateway-conformance-infra", "backend-tls-checks-certificate", []string{"abc.example.com"})
suite.Applier.MustApplyObjectsWithCleanup(t, suite.Client, suite.TimeoutConfig, []client.Object{caSecret}, suite.Cleanup)

tlog.Logf(t, "Test Setup: Ensuring Gateways and Pods from base manifests are ready")
namespaces := []string{
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.22.0
require (
github.com/ahmetb/gen-crd-api-reference-docs v0.3.0
github.com/miekg/dns v1.1.58
github.com/paultag/sniff v0.0.0-20200207005214-cf7e4d167732
github.com/stretchr/testify v1.9.0
golang.org/x/net v0.24.0
golang.org/x/sync v0.7.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8
github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs=
github.com/onsi/gomega v1.33.0 h1:snPCflnZrpMsy94p4lXVEkHo12lmPnc3vY5XBbreexE=
github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY=
github.com/paultag/sniff v0.0.0-20200207005214-cf7e4d167732 h1:nkseUkzjazCNyGhkRwnJ1OiHSwMXazsJQx+Ci+oVLEM=
github.com/paultag/sniff v0.0.0-20200207005214-cf7e4d167732/go.mod h1:J3XXNGJINXLa4yIivdUT0Ad/srv2q0pSOWbbm6El2EY=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
Expand Down

0 comments on commit 76c8e10

Please sign in to comment.