Add an integration test for AppArmor mediating activation
authorSimon McVittie <simon.mcvittie@collabora.co.uk>
Mon, 21 Nov 2016 20:46:17 +0000 (20:46 +0000)
committerSimon McVittie <simon.mcvittie@collabora.co.uk>
Mon, 28 Nov 2016 12:11:52 +0000 (12:11 +0000)
This requires libapparmor 2.10, for aa_features_new_from_kernel()
and related functions.

Signed-off-by: Simon McVittie <simon.mcvittie@collabora.co.uk>
Reviewed-by: Philip Withnall <philip.withnall@collabora.co.uk>
Bug: https://bugs.freedesktop.org/show_bug.cgi?id=98666

configure.ac
test/Makefile.am
test/data/dbus-installed-tests.aaprofile.in [new file with mode: 0644]
test/data/systemd-activation/com.example.ReceiveDeniedByAppArmorLabel.service.in [new file with mode: 0644]
test/data/systemd-activation/com.example.SendDeniedByAppArmorLabel.service.in [new file with mode: 0644]
test/data/systemd-activation/com.example.SendDeniedByAppArmorName.service [new file with mode: 0644]
test/sd-activation.c
test/test-apparmor-activation.sh [new file with mode: 0644]

index 73cecae..cdcad36 100644 (file)
@@ -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],
index 1eb086d..eb22122 100644 (file)
@@ -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 += \
@@ -201,10 +216,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)
 
@@ -236,6 +253,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 \
@@ -321,7 +353,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)
@@ -347,6 +382,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 \
@@ -432,6 +470,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 \
@@ -574,7 +613,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 (file)
index 0000000..de34c2d
--- /dev/null
@@ -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 <tunables/global>
+#include <tunables/sys>
+
+@DBUS_TEST_EXEC@/test-apparmor-activation {
+  #include <abstractions/apparmor_api/change_profile>
+  #include <abstractions/apparmor_api/find_mountpoint>
+  #include <abstractions/apparmor_api/is_enabled>
+  #include <abstractions/base>
+
+  # 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 <abstractions/apparmor_api/change_profile>
+    #include <abstractions/base>
+
+    /** 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 <abstractions/apparmor_api/change_profile>
+    #include <abstractions/base>
+
+    /** 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 <abstractions/apparmor_api/change_profile>
+    #include <abstractions/base>
+
+    /** 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 <abstractions/apparmor_api/change_profile>
+    #include <abstractions/base>
+
+    /** 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 (file)
index 0000000..295253e
--- /dev/null
@@ -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 (file)
index 0000000..882531b
--- /dev/null
@@ -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 (file)
index 0000000..f47674a
--- /dev/null
@@ -0,0 +1,4 @@
+[D-BUS Service]
+Name=com.example.SendDeniedByAppArmorName
+Exec=/bin/false SendDeniedByAppArmorName
+SystemdService=dbus-com.example.SendDeniedByAppArmorName.service
index 9b2a5bb..f296d32 100644 (file)
@@ -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.
 
 #include <config.h>
 
+#include <errno.h>
+#include <unistd.h>
 #include <string.h>
+#include <sys/types.h>
+
+#if defined(HAVE_APPARMOR) && defined(DBUS_TEST_APPARMOR_ACTIVATION)
+#include <sys/apparmor.h>
+#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 (file)
index 0000000..ffc1253
--- /dev/null
@@ -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: