From 68be24285013cd7b3b4aa8027ad773a3b790a24c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Budai?= Date: Wed, 23 Sep 2020 10:04:28 +0200 Subject: [PATCH] tests: introduce auth tests This commit introduces a new test binary responsible for testing TLS authentication. Currently, it covers both remote worker API and Koji API. It tests that the server refuses certificates issued by an untrusted CA or self-signed ones. Also, it tests that the certificate is issued for an allowed domain. TODO: certs with subject alternative name are currently not used in tests. They should work just right, but a proper testing requires more tinkering with OpenSSL than I'm willing to accept at this time --- Makefile | 1 + cmd/osbuild-auth-tests/main_test.go | 250 +++++++++++++++++++++++++ cmd/osbuild-composer/main.go | 4 +- golang-github-osbuild-composer.spec | 2 + osbuild-composer.spec | 4 +- schutzbot/run_base_tests.sh | 1 + test/image-tests/osbuild-composer.toml | 4 +- 7 files changed, 262 insertions(+), 4 deletions(-) create mode 100644 cmd/osbuild-auth-tests/main_test.go diff --git a/Makefile b/Makefile index 8093f8b97..b92ca985c 100644 --- a/Makefile +++ b/Makefile @@ -119,6 +119,7 @@ build: go test -c -tags=integration -o osbuild-dnf-json-tests ./cmd/osbuild-dnf-json-tests/main_test.go go test -c -tags=integration -o osbuild-image-tests ./cmd/osbuild-image-tests/ go test -c -tags=integration -o osbuild-composer-cloud-tests ./cmd/osbuild-composer-cloud-tests/main_test.go + go test -c -tags=integration -o osbuild-auth-tests ./cmd/osbuild-auth-tests/ .PHONY: install install: diff --git a/cmd/osbuild-auth-tests/main_test.go b/cmd/osbuild-auth-tests/main_test.go new file mode 100644 index 000000000..f84f29fc1 --- /dev/null +++ b/cmd/osbuild-auth-tests/main_test.go @@ -0,0 +1,250 @@ +// +build integration + +package main + +import ( + "crypto/tls" + "crypto/x509" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "os/exec" + "path" + "testing" + + "github.com/stretchr/testify/require" +) + +type connectionConfig struct { + CACertFile string + ClientKeyFile string + ClientCertFile string +} + +func createTLSConfig(config *connectionConfig) (*tls.Config, error) { + caCertPEM, err := ioutil.ReadFile(config.CACertFile) + if err != nil { + return nil, err + } + + roots := x509.NewCertPool() + ok := roots.AppendCertsFromPEM(caCertPEM) + if !ok { + return nil, errors.New("failed to append root certificate") + } + + cert, err := tls.LoadX509KeyPair(config.ClientCertFile, config.ClientKeyFile) + if err != nil { + return nil, err + } + + return &tls.Config{ + RootCAs: roots, + Certificates: []tls.Certificate{cert}, + }, nil +} + +type certificateKeyPair struct { + baseDir string +} + +func (ckp certificateKeyPair) remove() { + err := os.RemoveAll(ckp.baseDir) + if err != nil { + log.Printf("cannot delete the certificate key pair: %v", err) + } +} + +func (ckp certificateKeyPair) certificate() string { + return path.Join(ckp.baseDir, "crt") +} + +func (ckp certificateKeyPair) key() string { + return path.Join(ckp.baseDir, "key") +} + +func newCertificateKeyPair(CA, CAkey, subj string) (*certificateKeyPair, error) { + dir, err := ioutil.TempDir("", "osbuild-auth-tests-") + if err != nil { + return nil, fmt.Errorf("cannot create a temporary directory for the certificate: %v", err) + } + + ckp := certificateKeyPair{baseDir: dir} + certificateRequest := path.Join(dir, "csr") + + cmd := exec.Command( + "openssl", "req", "-new", "-nodes", + "-subj", subj, + "-keyout", ckp.key(), + "-out", certificateRequest, + ) + + err = cmd.Run() + if err != nil { + return nil, fmt.Errorf("cannot generate a private key and a certificate request: %v", err) + } + + defer os.Remove(certificateRequest) + + cmd = exec.Command( + "openssl", "x509", "-req", "-CAcreateserial", + "-in", certificateRequest, + "-CA", CA, + "-CAkey", CAkey, + "-out", ckp.certificate(), + ) + err = cmd.Run() + if err != nil { + return nil, fmt.Errorf("cannot sign the certificate: %v", err) + } + + return &ckp, nil +} + +func newSelfSignedCertificateKeyPair(subj string) (*certificateKeyPair, error) { + dir, err := ioutil.TempDir("", "osbuild-auth-tests-") + if err != nil { + return nil, fmt.Errorf("cannot create a temporary directory for the certificate: %v", err) + } + + ckp := certificateKeyPair{baseDir: dir} + + cmd := exec.Command( + "openssl", "req", "-nodes", "-x509", + "-subj", subj, + "-out", ckp.certificate(), + "-keyout", ckp.key(), + ) + err = cmd.Run() + if err != nil { + return nil, fmt.Errorf("cannot generate a self-signed certificate: %v", err) + } + + return &ckp, nil +} + +func TestWorkerAPIAuth(t *testing.T) { + t.Run("certificate signed by a trusted CA", func(t *testing.T) { + cases := []struct { + caseDesc string + subj string + success bool + }{ + {"valid CN 1", "/CN=worker.osbuild.org", true}, + {"valid CN 2", "/CN=localhost", true}, + {"invalid CN", "/CN=example.com", false}, + } + + for _, c := range cases { + t.Run(c.caseDesc, func(t *testing.T) { + ckp, err := newCertificateKeyPair("/etc/osbuild-composer/ca-crt.pem", "/etc/osbuild-composer/ca-key.pem", c.subj) + require.NoError(t, err) + defer ckp.remove() + + testRoute(t, "https://localhost:8700/status", ckp, c.success) + }) + } + }) + + t.Run("certificate signed by an untrusted CA", func(t *testing.T) { + // generate a new CA + ca, err := newSelfSignedCertificateKeyPair("/CN=osbuild.org") + require.NoError(t, err) + defer ca.remove() + + // create a new certificate and signed it with the new CA + ckp, err := newCertificateKeyPair(ca.certificate(), ca.key(), "/CN=localhost") + require.NoError(t, err) + defer ckp.remove() + + testRoute(t, "https://localhost:8700/status", ckp, false) + }) + + t.Run("self-signed certificate", func(t *testing.T) { + // generate a new self-signed certificate + ckp, err := newSelfSignedCertificateKeyPair("/CN=osbuild.org") + require.NoError(t, err) + defer ckp.remove() + + testRoute(t, "https://localhost:8700/status", ckp, false) + }) +} + +func TestKojiAPIAuth(t *testing.T) { + t.Run("certificate signed by a trusted CA", func(t *testing.T) { + cases := []struct { + caseDesc string + subj string + success bool + }{ + {"valid CN 1", "/CN=worker.osbuild.org", true}, + {"valid CN 2", "/CN=localhost", true}, + {"invalid CN", "/CN=example.com", false}, + } + + for _, c := range cases { + t.Run(c.caseDesc, func(t *testing.T) { + ckp, err := newCertificateKeyPair("/etc/osbuild-composer/ca-crt.pem", "/etc/osbuild-composer/ca-key.pem", c.subj) + require.NoError(t, err) + defer ckp.remove() + + testRoute(t, "https://localhost/status", ckp, c.success) + }) + } + }) + + t.Run("certificate signed by an untrusted CA", func(t *testing.T) { + // generate a new CA + ca, err := newSelfSignedCertificateKeyPair("/CN=osbuild.org") + require.NoError(t, err) + defer ca.remove() + + // create a new certificate and signed it with the new CA + ckp, err := newCertificateKeyPair(ca.certificate(), ca.key(), "/CN=localhost") + require.NoError(t, err) + defer ckp.remove() + + testRoute(t, "https://localhost/status", ckp, false) + }) + + t.Run("self-signed certificate", func(t *testing.T) { + // generate a new self-signed certificate + ckp, err := newSelfSignedCertificateKeyPair("/CN=osbuild.org") + require.NoError(t, err) + defer ckp.remove() + + testRoute(t, "https://localhost/status", ckp, false) + }) +} + +func testRoute(t *testing.T, route string, ckp *certificateKeyPair, expectSuccess bool) { + tlsConfig, err := createTLSConfig(&connectionConfig{ + CACertFile: "/etc/osbuild-composer/ca-crt.pem", + ClientKeyFile: ckp.key(), + ClientCertFile: ckp.certificate(), + }) + require.NoError(t, err) + + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.TLSClientConfig = tlsConfig + client := http.Client{Transport: transport} + + response, err := client.Get(route) + if expectSuccess { + require.NoError(t, err) + + var status struct { + Status string `json:"status"` + } + err := json.NewDecoder(response.Body).Decode(&status) + require.NoError(t, err) + + require.Equal(t, "OK", status.Status) + } else { + require.Error(t, err) + } +} diff --git a/cmd/osbuild-composer/main.go b/cmd/osbuild-composer/main.go index 920e74a60..df36f640e 100644 --- a/cmd/osbuild-composer/main.go +++ b/cmd/osbuild-composer/main.go @@ -61,7 +61,9 @@ func createTLSConfig(c *connectionConfig) (*tls.Config, error) { VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { for _, chain := range verifiedChains { for _, domain := range c.AllowedDomains { - return chain[0].VerifyHostname(domain) + if chain[0].VerifyHostname(domain) == nil { + return nil + } } } diff --git a/golang-github-osbuild-composer.spec b/golang-github-osbuild-composer.spec index d9dc5e3c5..9c058a1cf 100644 --- a/golang-github-osbuild-composer.spec +++ b/golang-github-osbuild-composer.spec @@ -96,6 +96,7 @@ go test -c -tags=integration -ldflags="${TEST_LDFLAGS}" -o _bin/osbuild-tests %{ go test -c -tags=integration -ldflags="${TEST_LDFLAGS}" -o _bin/osbuild-dnf-json-tests %{goipath}/cmd/osbuild-dnf-json-tests go test -c -tags=integration -ldflags="${TEST_LDFLAGS}" -o _bin/osbuild-weldr-tests %{goipath}/internal/client/ go test -c -tags=integration -ldflags="${TEST_LDFLAGS}" -o _bin/osbuild-image-tests %{goipath}/cmd/osbuild-image-tests +go test -c -tags=integration -ldflags="${TEST_LDFLAGS}" -o _bin/osbuild-auth-tests %{goipath}/cmd/osbuild-auth-tests %install install -m 0755 -vd %{buildroot}%{_libexecdir}/osbuild-composer @@ -119,6 +120,7 @@ install -m 0755 -vp _bin/osbuild-tests %{buildroot}%{_libex install -m 0755 -vp _bin/osbuild-weldr-tests %{buildroot}%{_libexecdir}/tests/osbuild-composer/ install -m 0755 -vp _bin/osbuild-dnf-json-tests %{buildroot}%{_libexecdir}/tests/osbuild-composer/ install -m 0755 -vp _bin/osbuild-image-tests %{buildroot}%{_libexecdir}/tests/osbuild-composer/ +install -m 0755 -vp _bin/osbuild-auth-tests %{buildroot}%{_libexecdir}/tests/osbuild-composer/ install -m 0755 -vp tools/image-info %{buildroot}%{_libexecdir}/osbuild-composer/ install -m 0755 -vd %{buildroot}%{_datadir}/tests/osbuild-composer diff --git a/osbuild-composer.spec b/osbuild-composer.spec index 282462a43..621fd17f5 100644 --- a/osbuild-composer.spec +++ b/osbuild-composer.spec @@ -118,6 +118,7 @@ go test -c -tags=integration -ldflags="${TEST_LDFLAGS}" -o _bin/osbuild-dnf-json go test -c -tags=integration -ldflags="${TEST_LDFLAGS}" -o _bin/osbuild-weldr-tests %{goipath}/internal/client/ go test -c -tags=integration -ldflags="${TEST_LDFLAGS}" -o _bin/osbuild-image-tests %{goipath}/cmd/osbuild-image-tests go test -c -tags=integration -ldflags="${TEST_LDFLAGS}" -o _bin/osbuild-composer-cloud-tests %{goipath}/cmd/osbuild-composer-cloud-tests +go test -c -tags=integration -ldflags="${TEST_LDFLAGS}" -o _bin/osbuild-auth-tests %{goipath}/cmd/osbuild-auth-tests %endif @@ -156,8 +157,9 @@ install -m 0755 -vp _bin/osbuild-tests %{buildroot}%{_l install -m 0755 -vp _bin/osbuild-weldr-tests %{buildroot}%{_libexecdir}/tests/osbuild-composer/ install -m 0755 -vp _bin/osbuild-dnf-json-tests %{buildroot}%{_libexecdir}/tests/osbuild-composer/ install -m 0755 -vp _bin/osbuild-image-tests %{buildroot}%{_libexecdir}/tests/osbuild-composer/ +install -m 0755 -vp _bin/osbuild-composer-cloud-tests %{buildroot}%{_libexecdir}/tests/osbuild-composer/ +install -m 0755 -vp _bin/osbuild-auth-tests %{buildroot}%{_libexecdir}/tests/osbuild-composer/ install -m 0755 -vp tools/image-info %{buildroot}%{_libexecdir}/osbuild-composer/ -install -m 0755 -vp _bin/osbuild-composer-cloud-tests %{buildroot}%{_libexecdir}/tests/osbuild-composer/ install -m 0755 -vd %{buildroot}%{_datadir}/tests/osbuild-composer install -m 0644 -vp test/azure-deployment-template.json %{buildroot}%{_datadir}/tests/osbuild-composer/ diff --git a/schutzbot/run_base_tests.sh b/schutzbot/run_base_tests.sh index e460367cb..30b195cd9 100755 --- a/schutzbot/run_base_tests.sh +++ b/schutzbot/run_base_tests.sh @@ -11,6 +11,7 @@ TEST_CASES=( "osbuild-weldr-tests" "osbuild-dnf-json-tests" "osbuild-tests" + "osbuild-auth-tests" ) # Print out a nice test divider so we know when tests stop and start. diff --git a/test/image-tests/osbuild-composer.toml b/test/image-tests/osbuild-composer.toml index 455a9eada..51f9014fd 100644 --- a/test/image-tests/osbuild-composer.toml +++ b/test/image-tests/osbuild-composer.toml @@ -1,9 +1,9 @@ [koji] -allowed_domains = [ "localhost", "*.osbuild.org" ] +allowed_domains = [ "localhost", "worker.osbuild.org" ] [koji.servers.localhost.kerberos] principal = "osbuild-krb@LOCAL" keytab = "/etc/osbuild-composer/client.keytab" [worker] -allowed_domains = [ "localhost", "*.osbuild.org" ] +allowed_domains = [ "localhost", "worker.osbuild.org" ]