diff --git a/docs/service_spec.md b/docs/service_spec.md index d0acce6..9db5763 100644 --- a/docs/service_spec.md +++ b/docs/service_spec.md @@ -170,6 +170,16 @@ If specified, the SDK will send a `tls` object containing a `skipVerifyPeer` pro true to indicate that verification should be skipped. This should allow the SDK to connect to the test harness over HTTPS even though the test harness's certificate is self-signed. +#### Capability `"tls:custom-ca"` + +This means the SDK is capable of configuring its trust store to use a specific CA file. This can be useful for +customers who have their own, internal certificate authority, and want to establish authenticity of their servers when +using the SDK. + +If specified, the SDK will send a `tls` object containing a `customCAFile` property. The property will be set to a +a file path containing one or more PEM-encoded x509 certificates. The SDK should configure its TLS stack to use +this file when verifying the peer's certificate chain. + ### Stop test service: `DELETE /` The test harness sends this request at the end of a test run if you have specified `--stop-service-at-end` on the [command line](./running.md). The test service should simply quit. This is a convenience so CI scripts can simply start the test service in the background and assume it will be stopped for them. @@ -223,6 +233,10 @@ A `POST` request indicates that the test harness wants to start an instance of t * `afterEvaluation` (string, optional): The error/exception message that should be generated in the `afterEvaluation` stage of the test hook. * `tls` (object, optional): If specified, contains configuration for establishing TLS connections. * `skipVerifyPeer` (bool, optional): If true, the SDK's TLS configuration should be set skip verification of the peer. If false or omitted, the SDK should perform peer verification. + * `customCAFile` (string, optional): If set, contains a file path pointing to a custom CA file that should be + used by the SDK to verify the peer's certificate chain. The file will contain one or more PEM-encoded x509 + certificates. The root of the certificate chain presented to the SDK during the TLS handshake must be signed by + one of these CA certificates. The response to a valid request is any HTTP `2xx` status, with a `Location` header whose value is the URL of the test service resource representing this SDK client instance (that is, the one that would be used for "Close client" or "Send command" as described below). diff --git a/framework/capabilities.go b/framework/capabilities.go index e600d8a..d80f98c 100644 --- a/framework/capabilities.go +++ b/framework/capabilities.go @@ -11,6 +11,20 @@ func (cs Capabilities) Has(name string) bool { return slices.Contains(cs, name) } +// HasAll returns true if the specifies strings all appear in the list. +func (cs Capabilities) HasAll(names ...string) bool { + caps := make(map[string]struct{}) + for _, c := range cs { + caps[c] = struct{}{} + } + for _, name := range names { + if _, ok := caps[name]; !ok { + return false + } + } + return true +} + // HasAny returns true if any of the specified strings appear in the list. func (cs Capabilities) HasAny(names ...string) bool { caps := make(map[string]struct{}) diff --git a/framework/harness/certificate/ca.conf b/framework/harness/certificate/ca.conf new file mode 100644 index 0000000..cf5d53c --- /dev/null +++ b/framework/harness/certificate/ca.conf @@ -0,0 +1,9 @@ +[req] +x509_extensions = v3_ca + +[v3_ca] +keyUsage=critical,keyCertSign +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid:always,issuer:always +basicConstraints=critical,CA:true,pathlen:0 +extendedKeyUsage=serverAuth diff --git a/framework/harness/certificate/ca_private.pem b/framework/harness/certificate/ca_private.pem new file mode 100644 index 0000000..ee24f15 --- /dev/null +++ b/framework/harness/certificate/ca_private.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +4fmao3ypheGgGVOiXRyo+xMmwpsgUA== +-----END PRIVATE KEY----- diff --git a/framework/harness/certificate/ca_public.pem b/framework/harness/certificate/ca_public.pem new file mode 100644 index 0000000..dbfb8db --- /dev/null +++ b/framework/harness/certificate/ca_public.pem @@ -0,0 +1,36 @@ +-----BEGIN CERTIFICATE----- +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 +Y7pIypZcXjOD91lklxuQHEpxPmtZ +-----END CERTIFICATE----- diff --git a/framework/harness/certificate/cert.crt b/framework/harness/certificate/cert.crt deleted file mode 100644 index 06b6a28..0000000 --- a/framework/harness/certificate/cert.crt +++ /dev/null @@ -1,35 +0,0 @@ ------BEGIN CERTIFICATE----- -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 -N1ulz1o= ------END CERTIFICATE----- diff --git a/framework/harness/certificate/cert.key b/framework/harness/certificate/cert.key deleted file mode 100644 index 2211ebf..0000000 --- a/framework/harness/certificate/cert.key +++ /dev/null @@ -1,52 +0,0 @@ ------BEGIN PRIVATE KEY----- -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 -Nkbac0ZTJ0E3kyWg5uJraL00hbGE3oTk ------END PRIVATE KEY----- diff --git a/framework/harness/certificate/gen.sh b/framework/harness/certificate/gen.sh index 3728c63..0552467 100755 --- a/framework/harness/certificate/gen.sh +++ b/framework/harness/certificate/gen.sh @@ -2,8 +2,38 @@ set -e -# Generates a certificate for testing purposes only. Validity 10 years. -# Expires: May 7, 2034 +# Generates a certificate chain for testing purposes only. Validity 10 years. # If tests start failing because this expires, congratulations! +# +# The chain consists of a self-signed CA, followed by a CA-signed leaf certificate. +# The CA cert is generated with extra config found in ca.conf. +# +# The concatenation of the leaf + CA is loaded into the test harness's +# http server via http.ListenAndServeTLS, along with the leaf private key. +# +# At test time, the SDK-under-test will be explicitly configured to trust the CA. -openssl req -new -newkey rsa:4096 -x509 -sha256 -days 3650 -nodes -out cert.crt -keyout cert.key +leaf_cert=leaf_public.pem +leaf_private_key=leaf_private.pem + +ca_cert=ca_public.pem +ca_private_key=ca_private.pem + +host=localhost +certValidityDays=3650 + +# Create CA +openssl req -newkey rsa:4096 -keyout "${ca_private_key}" -x509 -new -nodes -out "${ca_cert}" \ + -subj "/OU=SDKTeam/O=LaunchDarkly/L=Oakland/ST=California/C=US" -days "${certValidityDays}" \ + -config ca.conf + +# Create Cert Signing Request +openssl req -new -newkey rsa:4096 -nodes -keyout "${leaf_private_key}" -out csr.pem \ + -subj "/CN=${host}/OU=SDKTeam/O=LaunchDarkly/L=Oakland/ST=California/C=US" + +# Sign Cert +openssl x509 -req -in csr.pem -CA "${ca_cert}" -CAkey "${ca_private_key}" -CAcreateserial -out "${leaf_cert}" \ + -days "${certValidityDays}" + +rm ca_public.srl +rm csr.pem diff --git a/framework/harness/certificate/leaf_private.pem b/framework/harness/certificate/leaf_private.pem new file mode 100644 index 0000000..37bcf9e --- /dev/null +++ b/framework/harness/certificate/leaf_private.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +Lie6kBC6bw1pF0CK23M7P53F8Oe49Q== +-----END PRIVATE KEY----- diff --git a/framework/harness/certificate/leaf_public.pem b/framework/harness/certificate/leaf_public.pem new file mode 100644 index 0000000..10a7374 --- /dev/null +++ b/framework/harness/certificate/leaf_public.pem @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFnjCCA4agAwIBAgIURlckMOOc0gQc/HMnyUpihcArUSMwDQYJKoZIhvcNAQEL +BQAwXTEQMA4GA1UECwwHU0RLVGVhbTEVMBMGA1UECgwMTGF1bmNoRGFya2x5MRAw +DgYDVQQHDAdPYWtsYW5kMRMwEQYDVQQIDApDYWxpZm9ybmlhMQswCQYDVQQGEwJV +UzAeFw0yNDA1MjExODUwMDNaFw0zNDA1MTkxODUwMDNaMHExEjAQBgNVBAMMCWxv +Y2FsaG9zdDEQMA4GA1UECwwHU0RLVGVhbTEVMBMGA1UECgwMTGF1bmNoRGFya2x5 +MRAwDgYDVQQHDAdPYWtsYW5kMRMwEQYDVQQIDApDYWxpZm9ybmlhMQswCQYDVQQG +EwJVUzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKj3mfoMNKcEHhTJ +67uo2+LEUQf8LEWafd7gJiSIz2yK0LjwmE6JI4BbjBFIS12ODduPh7DiA9LpOY5R +ob16yv+lSYvVhcIm+MmBCMPtz7CzG0nbZAu3ZTs3Ukfu1ngOh9SPfJ1z3Gp20M64 +nar1G+oo/ey464IOBNAwwG20y9XSoMD86/rHWXGAKLa9RZSIvc+M1zR9otPeFWyx +Hes4vaX8DUjuI/+clsJdZWTIrOEF7B+GXytBndyjwmZUN005EFmzrLlk2WE8IJj4 +NLx5cA05xhExzlwIhC70678s6pvXlB5rCNrNkiAUBfSuEmE7D0kyu+5cUjLWOVQu +bPLrL+JbChw3lYCQtj+VlDltIEDyee6EgJeHwWANXM8ringodpoa6ta+hGf2egWx +IMs6vbSRdg2e3x9BvvWfy8JwRrSyJ+y7GeWgNYzv996QD8WZuUCqdZwfLyfxlbeq +AqKC5MUhp2C89QInyK059PG51jzmJsmkSscZZSlIJmZOj8vtAaUMaEaQb1rS9UT9 +Zun+PFn3tj3HD89hohFN+65KuLeHcGMyxRW55AA2oaLroa2RVoUPMSyBVrJbYaib +VBSmFaGJf+vm6yOcLWQF5C3UfalO/nlKE7zus78tO51ojERaTTYkEhujtCSsJ3Ll +R62RnbLMv0qj8hONjRzIrTBgxbgnAgMBAAGjQjBAMB0GA1UdDgQWBBQ3WNXo9uw1 +l3gO05v6kRZfJ70XwTAfBgNVHSMEGDAWgBQWQtW9VcMPN75YgnuAosApj6p4GzAN +BgkqhkiG9w0BAQsFAAOCAgEAU8mhKrjMwYkbksgn2Qw0did6Vafadq2bLzCm6jhJ +OCs7RdW/1rB2KyzePeCT7TiP6QO/UeOg3hC9eBjb40jRAy7n6DE34/CqzCjOCydP +6cHCq4oqeFjAFPjrGcHur4gigM0dA27K90rFeGKlY9tu7shYZBPNGrGJZO+j5quU +0+UwzCu5DqmrdCVDY4U0TG+g/2PdHCW2lsHyAhI1lcMAapsPP9UYznTkBvxsLFne +ZDMoSDRlXxf6Tl4LgiISLVskLTRDMuTIrqxjtj7XuL2jLjJSvF6xpUjM2XEjGHGs +HjiIQZr/cNgP/t1A4irbLYHO0baD3l5fj8UVQynkG+xvqM79gTUOK7z5Baz/cVgN +oLAEDU3ejOEcJv0cPLD5gSYIn/oVDpHfpSVp8iBnfyD0Y4dY2JykfgZ1y2yjYc88 +EtiFmab4qXM2KxN2tOSXwTnby0A1vh59Qxnn1ZKiB9WH4EPpKkmzR85qHR+GTbzJ +2a1/GNT6BGEEQb16P51GAR9QAkOinDj//aN8CnR7LpNT6y13udsKPdgy/j4UfvBd +jA4iUVzjLyC6GbsExqAj437Gax11IDUpK4PqxcfeMBOAlMXxC/aslhs+6fp9TnlQ +5HLqsqwKpW6x78H9gH+21PL7zVMdcb8uVWlRinvskEliMoostjvs5rajie0UihkL +IV4= +-----END CERTIFICATE----- diff --git a/framework/harness/harness.go b/framework/harness/harness.go index 6adeb57..bae2b76 100644 --- a/framework/harness/harness.go +++ b/framework/harness/harness.go @@ -1,6 +1,7 @@ package harness import ( + "bytes" _ "embed" "fmt" "io" @@ -14,41 +15,58 @@ import ( "github.com/launchdarkly/sdk-test-harness/v2/framework" ) -//go:embed certificate/cert.crt +//go:embed certificate/leaf_public.pem var certificate []byte -//go:embed certificate/cert.key +//go:embed certificate/leaf_private.pem var privateKey []byte +//go:embed certificate/ca_public.pem +var caCertificate []byte + type certPaths struct { - cert string - key string + cert string + key string + caFile string } func (c *certPaths) cleanup() { _ = os.Remove(c.cert) _ = os.Remove(c.key) + _ = os.Remove(c.caFile) } -func exportCertificate() (*certPaths, error) { - cert, err := os.CreateTemp("", "sdk-test-harness-cert*") +func makeTempFile(pattern string, data []byte) (string, error) { + f, err := os.CreateTemp("", pattern) if err != nil { - return nil, fmt.Errorf("failed to create temp certificate file: %w", err) + return "", err + } + defer f.Close() //nolint: errcheck + if _, err := f.Write(data); err != nil { + return "", err } - if _, err := cert.Write(certificate); err != nil { - return nil, fmt.Errorf("failed to write certificate to temp file: %w", err) + return f.Name(), nil +} + +func exportCertChain() (*certPaths, error) { + chain := bytes.NewBuffer(certificate) + chain.Write(caCertificate) + + cert, err := makeTempFile("sdk-test-harness-cert*", chain.Bytes()) + if err != nil { + return nil, fmt.Errorf("failed to create temp certificate file: %w", err) } - _ = cert.Close() - key, err := os.CreateTemp("", "sdk-test-harness-cert-private-key*") + key, err := makeTempFile("sdk-test-harness-cert-private-key*", privateKey) if err != nil { return nil, fmt.Errorf("failed to create temp private key file: %w", err) } - if _, err := key.Write(privateKey); err != nil { - return nil, fmt.Errorf("failed to write private key to temp file: %w", err) + + ca, err := makeTempFile("sdk-test-harness-caFile-cert*", caCertificate) + if err != nil { + return nil, fmt.Errorf("failed to create temp caFile certificate file: %w", err) } - _ = key.Close() - return &certPaths{cert: cert.Name(), key: key.Name()}, nil + return &certPaths{cert: cert, key: key, caFile: ca}, nil } const httpListenerTimeout = time.Second * 10 @@ -66,6 +84,7 @@ type TestHarness struct { testServiceInfo serviceinfo.TestServiceInfo mockEndpoints *mockEndpointsManager logger framework.Logger + caFile string } // SetService tells the endpoint manager which protocol should be used when BaseURL() is called on a MockEndpoint. @@ -76,6 +95,12 @@ func (h *TestHarness) SetService(service string) { h.mockEndpoints.SetService(service) } +// CertificateAuthorityFile returns the file path of a CA cert used by the test harness when establishing a TLS +// connection with the SDK under test. +func (h *TestHarness) CertificateAuthorityFile() string { + return h.caFile +} + // NewTestHarness creates a TestHarness instance, and verifies that the test service // is responding by querying its status resource. It also starts an HTTP listener // on the specified port to receive callback requests. @@ -111,10 +136,11 @@ func NewTestHarness( } if testServiceInfo.Capabilities.HasAny(servicedef.CapabilityTLSSkipVerifyPeer, servicedef.CapabilityTLSVerifyPeer) { - certInfo, err := exportCertificate() + certInfo, err := exportCertChain() if err != nil { return nil, err } + h.caFile = certInfo.caFile startHTTPSServer(testHarnessPort+1, certInfo, http.HandlerFunc(h.serveHTTP)) } @@ -205,6 +231,9 @@ func startHTTPSServer(port int, cert *certPaths, handler http.Handler) { } go func() { defer cert.cleanup() + // If we use keep-alives, the server can try to reuse the connection which isn't + // desirable for testing purposes. + server.SetKeepAlivesEnabled(false) if err := server.ListenAndServeTLS(cert.cert, cert.key); err != nil { panic(err) } diff --git a/framework/ldtest/test_scope.go b/framework/ldtest/test_scope.go index e19110e..e505e3f 100644 --- a/framework/ldtest/test_scope.go +++ b/framework/ldtest/test_scope.go @@ -39,7 +39,7 @@ type TestConfiguration struct { // Context is an optional value of any type defined by the application which can be accessed from tests. Context interface{} - // Capabilities is a list of strings which are used by T.HasCapability and T.RequireCapability. + // Capabilities is a list of strings which are used T.RequireCapability. Capabilities []string } @@ -211,13 +211,20 @@ func (t *T) Capabilities() framework.Capabilities { return append(framework.Capabilities(nil), t.env.config.Capabilities...) } -// RequireCapability causes the test to be skipped if HasCapability(name) returns false. +// RequireCapability causes the test to be skipped if Capabilities().Has(name) returns false. func (t *T) RequireCapability(name string) { if !t.Capabilities().Has(name) { t.SkipWithReason(fmt.Sprintf("test service does not have capability %q", name)) } } +// RequireCapabilities causes the test to be skipped if Capabilities().HasAll(names) returns false. +func (t *T) RequireCapabilities(names ...string) { + if !t.Capabilities().HasAll(names...) { + t.SkipWithReason(fmt.Sprintf("test service does not have all of the required capabilities: %v", names)) + } +} + // Helper marks the function that calls it as a test helper that shouldn't appear in stacktraces. // Equivalent to Go's testing.T.Helper(). func (t *T) Helper() { diff --git a/sdktests/common_tests_base.go b/sdktests/common_tests_base.go index 0c065ff..bba3b13 100644 --- a/sdktests/common_tests_base.go +++ b/sdktests/common_tests_base.go @@ -112,6 +112,8 @@ func (c commonTestsBase) availableFlagRequestMethods() []flagRequestMethod { type transportProtocol struct { // Either http or https. protocol string + // Tag appended to the test name + tag string // A function that configures the SDK's TLS options. configurer SDKConfigurer } @@ -127,7 +129,7 @@ func (t transportProtocol) Run(tester *ldtest.T, action func(*ldtest.T)) { // Ensure that if some test fails/panics, we are back to using HTTP by default for the next one. defer requireContext(tester).harness.SetService("http") - tester.Run(t.protocol, func(tester *ldtest.T) { + tester.Run(t.tag, func(tester *ldtest.T) { requireContext(tester).harness.SetService(t.protocol) action(tester) }) @@ -137,7 +139,7 @@ func (t transportProtocol) Run(tester *ldtest.T, action func(*ldtest.T)) { func (c commonTestsBase) withHTTPSTransport(t *ldtest.T) transportProtocol { t.RequireCapability(servicedef.CapabilityTLSVerifyPeer) // SDKs must verify peers by default, there's nothing to configure. - return transportProtocol{"https", NoopConfigurer{}} + return transportProtocol{"https", "https-verify-peer", NoopConfigurer{}} } // Returns a transportProtocol that runs the test under HTTPS with peer verification disabled. @@ -149,7 +151,19 @@ func (c commonTestsBase) withHTTPSTransportSkipVerifyPeer(t *ldtest.T) transport }) return nil }) - return transportProtocol{"https", configurer} + return transportProtocol{"https", "https-skip-verify-peer", configurer} +} + +func (c commonTestsBase) withHTTPSTransportVerifyPeerCustomCA(t *ldtest.T, customCAFile string) transportProtocol { + t.RequireCapabilities(servicedef.CapabilityTLSCustomCA, servicedef.CapabilityTLSVerifyPeer) + configurer := helpers.ConfigOptionFunc[servicedef.SDKConfigParams](func(configOut *servicedef.SDKConfigParams) error { + configOut.TLS = o.Some(servicedef.SDKConfigTLSParams{ + SkipVerifyPeer: false, + CustomCAFile: customCAFile, + }) + return nil + }) + return transportProtocol{"https", "https-verify-peer-custom-ca", configurer} } // Returns the transports available for testing. For each transportProtocol returned, use the Run method @@ -160,11 +174,15 @@ func (c commonTestsBase) withAvailableTransports(t *ldtest.T) []transportProtoco // By default, tests are set up with http. Therefore, there's no need to specifically reconfigure the SDK. // If that changes in the future, this would need to be modified. configurers := []transportProtocol{ - {"http", NoopConfigurer{}}, + {"http", "http", NoopConfigurer{}}, } if t.Capabilities().Has(servicedef.CapabilityTLSSkipVerifyPeer) { configurers = append(configurers, c.withHTTPSTransportSkipVerifyPeer(t)) } + if t.Capabilities().HasAll(servicedef.CapabilityTLSCustomCA, servicedef.CapabilityTLSVerifyPeer) { + configurers = append(configurers, c.withHTTPSTransportVerifyPeerCustomCA(t, + requireContext(t).harness.CertificateAuthorityFile())) + } return configurers } diff --git a/sdktests/common_tests_events_request.go b/sdktests/common_tests_events_request.go index 638f3c6..8152755 100644 --- a/sdktests/common_tests_events_request.go +++ b/sdktests/common_tests_events_request.go @@ -45,12 +45,11 @@ func (c CommonEventTests) RequestMethodAndHeaders(t *ldtest.T, credential string } }) t.Run("invalid tls certificate", func(t *ldtest.T) { - // Setting up the data source *outside* the transport.Run so that it uses normal https transport and the - // data source connection can succeed. This is because we're trying to only test the TLS certificate verification - // logic that applies to sending events. - dataSource := NewSDKDataSource(t, nil) - c.withHTTPSTransport(t).Run(t, func(t *ldtest.T) { + //// It's not expected that the data source connection will succeed (since it's an https url, and the SDK's + //// default trust store won't contain the self-signed cert.) This test is only concerned with events; the + //// data source is being configured because it is required by the harness. + dataSource := NewSDKDataSource(t, nil) events := NewSDKEventSink(t) client := NewSDKClient(t, c.baseSDKConfigurationPlus(dataSource, events)...) diff --git a/servicedef/sdk_config.go b/servicedef/sdk_config.go index a35df58..b3ebcce 100644 --- a/servicedef/sdk_config.go +++ b/servicedef/sdk_config.go @@ -28,7 +28,8 @@ type SDKConfigParams struct { } type SDKConfigTLSParams struct { - SkipVerifyPeer bool `json:"skipVerifyPeer,omitempty"` + SkipVerifyPeer bool `json:"skipVerifyPeer,omitempty"` + CustomCAFile string `json:"customCAFile,omitempty"` } type SDKConfigServiceEndpointsParams struct { Streaming string `json:"streaming,omitempty"` diff --git a/servicedef/service_params.go b/servicedef/service_params.go index b58826f..81edbe4 100644 --- a/servicedef/service_params.go +++ b/servicedef/service_params.go @@ -47,6 +47,11 @@ const ( // skip the peer verification step. This allows the SDK to establish a connection with the test harness using // a self-signed certificate without a CA. Not all SDKs have this capability. CapabilityTLSSkipVerifyPeer = "tls:skip-verify-peer" + + // CapabilityTLSCustomCA means the SDK is capable of establishing a TLS session and configuring peer verification + // to use a custom CA certificate. The path to this CA cert is provided to the SDK. The SDK should then configure this + // path as the only CA cert in its trust store (rather than adding it to an existing trust store.) + CapabilityTLSCustomCA = "tls:custom-ca" ) type StatusRep struct {