From 021aa3bfec6f748524a4d3b690493774ad3f5e95 Mon Sep 17 00:00:00 2001 From: Simon McVittie Date: Mon, 21 Nov 2016 20:46:17 +0000 Subject: [PATCH 09/10] Add an integration test for AppArmor mediating activation This requires libapparmor 2.10, for aa_features_new_from_kernel() and related functions. Signed-off-by: Simon McVittie --- configure.ac | 2 +- test/Makefile.am | 43 +++++- test/data/dbus-installed-tests.aaprofile.in | 94 +++++++++++++ ...example.ReceiveDeniedByAppArmorLabel.service.in | 5 + ...om.example.SendDeniedByAppArmorLabel.service.in | 5 + .../com.example.SendDeniedByAppArmorName.service | 4 + test/sd-activation.c | 150 +++++++++++++++++++-- test/test-apparmor-activation.sh | 57 ++++++++ 8 files changed, 344 insertions(+), 16 deletions(-) create mode 100644 test/data/dbus-installed-tests.aaprofile.in create mode 100644 test/data/systemd-activation/com.example.ReceiveDeniedByAppArmorLabel.service.in create mode 100644 test/data/systemd-activation/com.example.SendDeniedByAppArmorLabel.service.in create mode 100644 test/data/systemd-activation/com.example.SendDeniedByAppArmorName.service create mode 100644 test/test-apparmor-activation.sh diff --git a/configure.ac b/configure.ac index f58827f..fffb1f9 100644 --- a/configure.ac +++ b/configure.ac @@ -1066,7 +1066,7 @@ fi AS_IF([test x$enable_apparmor = xno], [have_apparmor=no], [ - PKG_CHECK_MODULES([APPARMOR], [libapparmor >= 2.8.95], + PKG_CHECK_MODULES([APPARMOR], [libapparmor >= 2.10], [have_apparmor=yes], [have_apparmor=no]) AS_IF([test x$enable_apparmor = xauto && test x$have_apparmor = xno], diff --git a/test/Makefile.am b/test/Makefile.am index ac8c50b..3773132 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -12,6 +12,7 @@ AM_CPPFLAGS = \ -I$(top_srcdir) \ $(DBUS_STATIC_BUILD_CPPFLAGS) \ -DDBUS_COMPILATION \ + $(APPARMOR_CFLAGS) \ $(GLIB_CFLAGS) \ $(GIO_UNIX_CFLAGS) \ $(NULL) @@ -138,6 +139,8 @@ dist_testexec_SCRIPTS = testexec_PROGRAMS = testmeta_DATA = +installable_helpers = \ + $(NULL) installable_tests = \ test-shell \ test-printf \ @@ -149,6 +152,8 @@ installable_manual_tests = \ $(NULL) dist_installable_test_scripts = \ $(NULL) +dist_installed_test_scripts = \ + $(NULL) if DBUS_WIN installable_manual_tests += manual-paths @@ -171,6 +176,11 @@ installable_tests += \ $(NULL) if DBUS_UNIX +# These binaries are used in tests but are not themselves tests +installable_helpers += \ + test-apparmor-activation \ + $(NULL) + installable_tests += \ test-sd-activation \ $(NULL) @@ -179,6 +189,11 @@ dist_installable_test_scripts += \ test-dbus-daemon-fork.sh \ $(NULL) +# Only runnable when installed, not from the source tree +dist_installed_test_scripts += \ + test-apparmor-activation.sh \ + $(NULL) + # Testing dbus-launch relies on special code in that binary. if DBUS_ENABLE_EMBEDDED_TESTS dist_installable_test_scripts += \ @@ -196,10 +211,12 @@ endif DBUS_WITH_GLIB installable_test_meta = \ $(dist_installable_test_scripts:=.test) \ + $(dist_installed_test_scripts:=.test) \ $(installable_tests:=.test) \ $(NULL) installable_test_meta_with_config = \ $(dist_installable_test_scripts:=_with_config.test) \ + $(dist_installed_test_scripts:=_with_config.test) \ $(installable_tests:=_with_config.test) \ $(NULL) @@ -231,6 +248,21 @@ manual_authz_LDADD = \ $(GLIB_LIBS) \ $(NULL) +if DBUS_UNIX +test_apparmor_activation_CPPFLAGS = \ + $(AM_CPPFLAGS) \ + -DDBUS_TEST_APPARMOR_ACTIVATION \ + $(NULL) +test_apparmor_activation_SOURCES = \ + sd-activation.c \ + $(NULL) +test_apparmor_activation_LDADD = \ + libdbus-testutils.la \ + $(APPARMOR_LIBS) \ + $(GLIB_LIBS) \ + $(NULL) +endif + test_corrupt_SOURCES = corrupt.c test_corrupt_LDADD = \ libdbus-testutils.la \ @@ -316,7 +348,10 @@ TESTS += $(installable_tests) installcheck_tests += $(installable_tests) if DBUS_ENABLE_INSTALLED_TESTS - testexec_PROGRAMS += $(installable_tests) $(installable_manual_tests) + testexec_PROGRAMS += $(installable_helpers) + testexec_PROGRAMS += $(installable_manual_tests) + testexec_PROGRAMS += $(installable_tests) + dist_testexec_SCRIPTS += $(dist_installed_test_scripts) dist_testexec_SCRIPTS += $(dist_installable_test_scripts) testmeta_DATA += $(installable_test_meta) @@ -342,6 +377,9 @@ if DBUS_ENABLE_INSTALLED_TESTS endif DBUS_ENABLE_INSTALLED_TESTS in_data = \ + data/dbus-installed-tests.aaprofile.in \ + data/systemd-activation/com.example.ReceiveDeniedByAppArmorLabel.service.in \ + data/systemd-activation/com.example.SendDeniedByAppArmorLabel.service.in \ data/valid-config-files-system/debug-allow-all-fail.conf.in \ data/valid-config-files-system/debug-allow-all-pass.conf.in \ data/valid-config-files/debug-allow-all-sha1.conf.in \ @@ -427,6 +465,7 @@ static_data = \ data/sha-1/byte-messages.sha1 \ data/systemd-activation/com.example.ReceiveDenied.service \ data/systemd-activation/com.example.SendDenied.service \ + data/systemd-activation/com.example.SendDeniedByAppArmorName.service \ data/systemd-activation/com.example.SystemdActivatable1.service \ data/systemd-activation/com.example.SystemdActivatable2.service \ data/systemd-activation/com.example.SystemdActivatable3.service \ @@ -569,7 +608,7 @@ $(installable_test_meta_with_config): %_with_config.test: %$(EXEEXT) Makefile echo '[Test]'; \ echo 'Type=session'; \ echo 'Output=TAP'; \ - echo 'Exec=env DBUS_TEST_DATA=$(testexecdir)/data $(testexecdir)/$* --tap'; \ + echo 'Exec=env DBUS_TEST_EXEC=$(testexecdir) DBUS_TEST_DATA=$(testexecdir)/data $(testexecdir)/$* --tap'; \ ) > $@.tmp && mv $@.tmp $@ # Add rules for code-coverage testing, as defined by AX_CODE_COVERAGE diff --git a/test/data/dbus-installed-tests.aaprofile.in b/test/data/dbus-installed-tests.aaprofile.in new file mode 100644 index 0000000..de34c2d --- /dev/null +++ b/test/data/dbus-installed-tests.aaprofile.in @@ -0,0 +1,94 @@ +# Copyright © 2016 Collabora Ltd. +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +#include +#include + +@DBUS_TEST_EXEC@/test-apparmor-activation { + #include + #include + #include + #include + + # We aren't really confining this process seriously; allow most things. + /** mrix, + @{sys}/kernel/security/apparmor/** w, + dbus (send, receive, bind), + network, + signal, + + # "Hat" subprofile used for the part of the process that imitates a client + # trying to cause service activation via auto-starting. + ^caller { + #include + #include + + /** mrix, + @{sys}/kernel/security/apparmor/** w, + dbus (send, receive, bind), + network, + signal, + + deny dbus send peer=(label=@DBUS_TEST_EXEC@/test-apparmor-activation//com.example.SendDeniedByAppArmorLabel), + deny dbus send peer=(name=com.example.SendDeniedByAppArmorName), + } + + # Used when we check that XML-based policy still works. + ^com.example.ReceiveDenied { + #include + #include + + /** mrix, + @{sys}/kernel/security/apparmor/** w, + dbus, + network, + signal, + } + + # This one is never actually used, but needs to exist so ^caller can + # refer to it + ^com.example.SendDeniedByAppArmorLabel { + #include + #include + + /** mrix, + @{sys}/kernel/security/apparmor/** w, + dbus (send, receive, bind), + network, + signal, + } + + # "Hat" subprofile used for the part of the process that imitates a service + # that is not allowed to receive from the caller. + ^com.example.ReceiveDeniedByAppArmorLabel { + #include + #include + + /** mrix, + @{sys}/kernel/security/apparmor/** w, + dbus (send, receive, bind), + network, + signal, + + deny dbus receive peer=(label=@DBUS_TEST_EXEC@/test-apparmor-activation//caller), + } +} diff --git a/test/data/systemd-activation/com.example.ReceiveDeniedByAppArmorLabel.service.in b/test/data/systemd-activation/com.example.ReceiveDeniedByAppArmorLabel.service.in new file mode 100644 index 0000000..295253e --- /dev/null +++ b/test/data/systemd-activation/com.example.ReceiveDeniedByAppArmorLabel.service.in @@ -0,0 +1,5 @@ +[D-BUS Service] +Name=com.example.ReceiveDeniedByAppArmorLabel +Exec=/bin/false ReceiveDeniedByAppArmorLabel +SystemdService=dbus-com.example.ReceiveDeniedByAppArmorLabel.service +AssumedAppArmorLabel=@DBUS_TEST_EXEC@/test-apparmor-activation//com.example.ReceiveDeniedByAppArmorLabel diff --git a/test/data/systemd-activation/com.example.SendDeniedByAppArmorLabel.service.in b/test/data/systemd-activation/com.example.SendDeniedByAppArmorLabel.service.in new file mode 100644 index 0000000..882531b --- /dev/null +++ b/test/data/systemd-activation/com.example.SendDeniedByAppArmorLabel.service.in @@ -0,0 +1,5 @@ +[D-BUS Service] +Name=com.example.SendDeniedByAppArmorLabel +Exec=/bin/false SendDeniedByAppArmorLabel +SystemdService=dbus-com.example.SendDeniedByAppArmorLabel.service +AssumedAppArmorLabel=@DBUS_TEST_EXEC@/test-apparmor-activation//com.example.SendDeniedByAppArmorLabel diff --git a/test/data/systemd-activation/com.example.SendDeniedByAppArmorName.service b/test/data/systemd-activation/com.example.SendDeniedByAppArmorName.service new file mode 100644 index 0000000..f47674a --- /dev/null +++ b/test/data/systemd-activation/com.example.SendDeniedByAppArmorName.service @@ -0,0 +1,4 @@ +[D-BUS Service] +Name=com.example.SendDeniedByAppArmorName +Exec=/bin/false SendDeniedByAppArmorName +SystemdService=dbus-com.example.SendDeniedByAppArmorName.service diff --git a/test/sd-activation.c b/test/sd-activation.c index 9b2a5bb..f296d32 100644 --- a/test/sd-activation.c +++ b/test/sd-activation.c @@ -1,4 +1,7 @@ -/* Unit tests for systemd activation. +/* Unit tests for systemd activation, with or without AppArmor. + * + * We compile this source file twice: once with AppArmor support (if available) + * and once without. * * Copyright © 2010-2011 Nokia Corporation * Copyright © 2015 Collabora Ltd. @@ -26,7 +29,14 @@ #include +#include +#include #include +#include + +#if defined(HAVE_APPARMOR) && defined(DBUS_TEST_APPARMOR_ACTIVATION) +#include +#endif #include "test-utils-glib.h" @@ -196,6 +206,40 @@ static void setup (Fixture *f, gconstpointer context G_GNUC_UNUSED) { +#if defined(DBUS_TEST_APPARMOR_ACTIVATION) && !defined(HAVE_APPARMOR) + + g_test_skip ("AppArmor support not compiled"); + return; + +#else + +#if defined(DBUS_TEST_APPARMOR_ACTIVATION) + aa_features *features; + + if (!aa_is_enabled ()) + { + g_test_message ("aa_is_enabled() -> %s", g_strerror (errno)); + g_test_skip ("AppArmor not enabled"); + return; + } + + if (aa_features_new_from_kernel (&features) != 0) + { + g_test_skip ("Unable to check AppArmor features"); + return; + } + + if (!aa_features_supports (features, "dbus/mask/send") || + !aa_features_supports (features, "dbus/mask/receive")) + { + g_test_skip ("D-Bus send/receive mediation unavailable"); + aa_features_unref (features); + return; + } + + aa_features_unref (features); +#endif + f->ctx = test_main_context_get (); f->ge = NULL; @@ -208,8 +252,31 @@ setup (Fixture *f, if (f->address == NULL) return; +#if defined(DBUS_TEST_APPARMOR_ACTIVATION) + /* + * Make use of the fact that the LSM security label (and other process + * properties) that are used for access-control are whatever was current + * at the time the connection was opened. + * + * 42 is arbitrary. In a real use of AppArmor it would be a securely-random + * value, to prevent less-privileged code (that does not know the magic + * value) from changing back. + */ + if (aa_change_hat ("caller", 42) != 0) + g_error ("Unable to change profile to ...//^caller: %s", + g_strerror (errno)); +#endif + f->caller = test_connect_to_bus (f->ctx, f->address); f->caller_name = dbus_bus_get_unique_name (f->caller); + +#if defined(DBUS_TEST_APPARMOR_ACTIVATION) + if (aa_change_hat (NULL, 42) != 0) + g_error ("Unable to change back to initial profile: %s", + g_strerror (errno)); +#endif + +#endif } static void @@ -557,6 +624,7 @@ test_deny_send (Fixture *f, gconstpointer context) { DBusMessage *m; + const char *bus_name = context; if (f->address == NULL) return; @@ -567,7 +635,7 @@ test_deny_send (Fixture *f, f->caller_filter_added = TRUE; /* The sender sends a message to an activatable service. */ - m = dbus_message_new_method_call ("com.example.SendDenied", "/foo", + m = dbus_message_new_method_call (bus_name, "/foo", "com.example.bar", "Call"); if (m == NULL) g_error ("OOM"); @@ -575,8 +643,20 @@ test_deny_send (Fixture *f, dbus_connection_send (f->caller, m, NULL); dbus_message_unref (m); - /* Even before the fake systemd connects to the bus, we get an error - * back: activation is not allowed. */ + /* + * Even before the fake systemd connects to the bus, we get an error + * back: activation is not allowed. + * + * In the normal case, this is because the XML policy does not allow + * anyone to send messages to the bus name com.example.SendDenied. + * + * In the AppArmor case, this is because the AppArmor policy does not allow + * this process to send messages to the bus name + * com.example.SendDeniedByAppArmorName, or to the label + * @DBUS_TEST_EXEC@/com.example.SendDeniedByAppArmorLabel that we assume the + * service com.example.SendDeniedByAppArmorLabel will receive after systemd + * runs it. + */ while (f->caller_message == NULL) test_main_context_iterate (f->ctx, TRUE); @@ -593,6 +673,7 @@ test_deny_receive (Fixture *f, gconstpointer context) { DBusMessage *m; + const char *bus_name = context; if (f->address == NULL) return; @@ -602,9 +683,10 @@ test_deny_receive (Fixture *f, f->caller_filter_added = TRUE; - /* The sender sends a message to an activatable service. */ - m = dbus_message_new_method_call ("com.example.ReceiveDenied", "/foo", - "com.example.ReceiveDenied", "Call"); + /* The sender sends a message to an activatable service. + * We set the interface name equal to the bus name to make it + * easier to write the necessary policy rules. */ + m = dbus_message_new_method_call (bus_name, "/foo", bus_name, "Call"); if (m == NULL) g_error ("OOM"); @@ -631,16 +713,44 @@ test_deny_receive (Fixture *f, dbus_message_unref (m); /* systemd starts the activatable service. */ + +#if defined(DBUS_TEST_APPARMOR_ACTIVATION) && defined(HAVE_APPARMOR) + /* The use of 42 here is arbitrary, see setup(). */ + if (aa_change_hat (bus_name, 42) != 0) + g_error ("Unable to change profile to ...//^%s: %s", + bus_name, g_strerror (errno)); +#endif + f->activated = test_connect_to_bus (f->ctx, f->address); if (!dbus_connection_add_filter (f->activated, activated_filter, f, NULL)) g_error ("OOM"); f->activated_filter_added = TRUE; f->activated_name = dbus_bus_get_unique_name (f->activated); - take_well_known_name (f, f->activated, "com.example.ReceiveDenied"); - - /* We re-do the message matching, and now the message is - * forbidden by the receive policy. */ + take_well_known_name (f, f->activated, bus_name); + +#if defined(DBUS_TEST_APPARMOR_ACTIVATION) && defined(HAVE_APPARMOR) + if (aa_change_hat (NULL, 42) != 0) + g_error ("Unable to change back to initial profile: %s", + g_strerror (errno)); +#endif + + /* + * We re-do the message matching, and now the message is + * forbidden by the receive policy. + * + * In the normal case, this is because the XML policy does not allow + * receiving any message with interface com.example.ReceiveDenied. + * We can't use the recipient's bus name here because the XML policy + * has no syntax for preventing the owner of a name from receiving + * messages - that would be pointless, because the sender could just + * open another connection and not own the same name on that connection. + * + * In the AppArmor case, this is because the AppArmor policy does not allow + * receiving messages with interface com.example.ReceiveDeniedByAppArmor + * from a peer with the same label we have. Again, we can't use the + * recipient's bus name because there is no syntax for this. + */ while (f->caller_message == NULL) test_main_context_iterate (f->ctx, TRUE); @@ -707,10 +817,24 @@ main (int argc, setup, test_activation, teardown); g_test_add ("/sd-activation/uae", Fixture, NULL, setup, test_uae, teardown); - g_test_add ("/sd-activation/deny-send", Fixture, NULL, + g_test_add ("/sd-activation/deny-send", Fixture, + "com.example.SendDenied", + setup, test_deny_send, teardown); + g_test_add ("/sd-activation/deny-receive", Fixture, + "com.example.ReceiveDenied", + setup, test_deny_receive, teardown); + +#if defined(DBUS_TEST_APPARMOR_ACTIVATION) + g_test_add ("/sd-activation/apparmor/deny-send/by-label", Fixture, + "com.example.SendDeniedByAppArmorLabel", + setup, test_deny_send, teardown); + g_test_add ("/sd-activation/apparmor/deny-send/by-name", Fixture, + "com.example.SendDeniedByAppArmorName", setup, test_deny_send, teardown); - g_test_add ("/sd-activation/deny-receive", Fixture, NULL, + g_test_add ("/sd-activation/apparmor/deny-receive/by-label", Fixture, + "com.example.ReceiveDeniedByAppArmorLabel", setup, test_deny_receive, teardown); +#endif return g_test_run (); } diff --git a/test/test-apparmor-activation.sh b/test/test-apparmor-activation.sh new file mode 100644 index 0000000..ffc1253 --- /dev/null +++ b/test/test-apparmor-activation.sh @@ -0,0 +1,57 @@ +#!/bin/sh + +# Copyright © 2016 Collabora Ltd. +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +set -e + +if [ "x$(id -u)" != "x0" ]; then + echo "1..0 # SKIP - this test can only be run as root" + exit 0 +fi + +if [ -z "$DBUS_TEST_EXEC" ]; then + DBUS_TEST_EXEC="$(dirname "$0")" + if ! [ -x "$DBUS_TEST_EXEC/test-apparmor-activation" ]; then + echo "1..0 # SKIP - executable not found and DBUS_TEST_EXEC not set" + exit 0 + fi +fi + +if [ -z "$DBUS_TEST_DATA" ]; then + DBUS_TEST_DATA="$DBUS_TEST_EXEC/data" + if ! [ -e "$DBUS_TEST_DATA/dbus-installed-tests.aaprofile" ]; then + echo "1..0 # SKIP - required data not found and DBUS_TEST_DATA not set" + exit 0 + fi +fi + +echo "# Attempting to load AppArmor profiles" +if ! apparmor_parser --skip-cache --replace \ + "$DBUS_TEST_DATA/dbus-installed-tests.aaprofile"; then + echo "1..0 # SKIP - unable to load AppArmor profiles" + exit 0 +fi + +exec "$DBUS_TEST_EXEC/test-apparmor-activation" --tap "$@" + +# vim:set sts=4 sw=4 et: -- 2.10.2