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" ]