Testing PAM modules and applications in the Matrix
A new tool, called pam_wrapper, was developed by the article authors; it makes it easy to either test an application that uses pluggable authentication modules (PAM) to authenticate a user, or to develop test cases to make sure that a PAM module under development is working correctly. It is a tool that enables developers to create unit tests for their PAM-using code in a simple manner.
PAM is a layer of abstraction on top of Unix authentication. It is written so that applications don't have to worry about the underlying authentication scheme, which is implemented in a module. If you're not familiar with PAM you can learn more here.
A "PAM conversation" is part of the process of doing authentication using PAM. It is essentially a question and answer game between the user and the system that is being used to authenticate the user. Normally, it just asks for a username and password, but it could also ask the user for the username then ten questions about Star Wars before actually asking for the password and authenticating the user.
Pam_wrapper is a component of the cwrap project, which provides a set of tools that make testing easier. Due to its origin in the Samba project, cwrap is especially targeted at client/server testing. Pam_wrapper is a preloadable library similar to the other cwrap components.
About pam_wrapper
The authors are working on different software projects like Samba, sssd, and libssh. Samba and sssd provide PAM modules and, until now, there were no tests for authentication using the modules available. There was no easy way to achieve that without a fully configured environment, so tests were done by people who run the modules in production or by dedicated QA teams.The libssh project runs tests against the SSH daemon from the OpenSSH project. This was only possible in a special environment with root privileges. With pam_wrapper and the PAM module it provides, you can now run the OpenSSH daemon as a normal user performing the PAM conversation to test interactive logins. This means pam_wrapper is useful for both writing tests for PAM modules or using it to handle PAM conversations.
Testing either PAM modules or PAM applications does not require root privileges when using pam_wrapper. You can also set up a dummy user account database to test against.
Testing applications
In theory, testing PAM applications shouldn't require too much instrumentation. A PAM service file allows the administrator to specify a full path, which can point to a PAM module under test; both the PAM application itself and the module can usually run unprivileged. The problem is with the location that the PAM service files are loaded from — the directory (typically /etc/pam.d) is hardcoded in libpam.so during configure time, and there is no way to override it at runtime. The pam_wrapper library allows the developer to specify an alternate directory with PAM service files, which can point to different service configurations or include test modules. This also removes the requirement to run tests as root, because the test configurations can be stored under the UID running the test.
Pam_wrapper is a preloadable library. Preloading is a feature of the dynamic linker that loads the user-specified libraries before all others. Note that if you try to preload a library for binaries that have the suid or sgid bit set (see the chmod(1) man page), the user-specified preloaded libraries are ignored. The pam_wrapper library wraps all functions of libpam.so and allows you to define your own service directory for each test:
LD_PRELOAD=libpam_wrapper.so PAM_WRAPPER=1 \ PAM_WRAPPER_SERVICE_DIR=/path/to/servicedir ./myapplication
This command would run myapplication and tell libpam.so to read service files from the directory /path/to/servicedir instead of /etc/pam.d. The PAM_WRAPPER environment variable must be set to enable the library, which should restrict the ability to use it for attacks of any sort.
A service directory normally contains one file for the service that the test is being run against. For example, if you want to authenticate using sshd, your service file name would be sshd. In the file you need to specify which of the management groups the subsequent module is to be associated with. Valid entries are account, auth, password, and session.
The management groups handle different phases of the authentication process. The auth group modules manage authentication (i.e. if the user is who they claim to be), while the account group verifies that the user is permitted to do the action they are trying to do; it normally runs after authentication. The password group is used for password changes and the session group sets up the user environment — it can mount user-private directories, for example. They are described in the pam.d(5) man page.
Testing an application with pam_matrix
Another issue developers face when developing tests for PAM applications is that there must be some database that the tests authenticate against. A very simple test could use the pam_permit or pam_deny modules that either allow or deny all requests, but that doesn't provide tests that are like real deployments. Therefore, the pam_wrapper project added a simple PAM module called pam_matrix.so that will authenticate against a simple text database.
Let's assume you want to run tests against a server that requires PAM to authenticate users. This application uses PAM service file myapp. Normally, you would need a real user in the system with a password set — but this might not be possible in environments like Continuous Integration (CI) systems or on build hosts. Pam_wrapper and the pam_matrix module allow you to authenticate users via PAM without requiring an account on the local machine.
For that you need to create a service file that looks like this:
auth required pam_matrix.so passdb=/tmp/passdb account required pam_matrix.so passdb=/tmp/passdb password required pam_matrix.so passdb=/tmp/passdb session required pam_matrix.so passdb=/tmp/passdb
Save this file as myapp and place it in a directory. Later, this directory will be referenced in the PAM_WRAPPER_SERVICE_DIR variable. The passdb option defines a file that contains users with a plain-text password for a specified service. The syntax of the file is:
username:password:allowed_service
An example for that is:
bob:secret:myapp
As an alternative to using the passdb PAM module option, it's possible to specify the database location by setting the PAM_MATRIX_PASSWD environment variable.
Testing a module with libpamtest and pam_wrapper helper modules
Writing tests for PAM applications or modules can be a tedious task. Each test would have to implement some way of passing data like passwords to the PAM modules executing the test (probably via a conversation function), run the PAM conversation, and collect output from the module or application under test. To simplify writing these tests, we added a library called libpamtest to the pam_wrapper project. This library allows the test developer to avoid code duplication and boilerplate code, and focus on writing tests instead. The libpamtest library comes with fully documented C and Python APIs.
Each libpamtest-driven test is defined by one or more instances of the structure pam_testcase that describes what kind of test is supposed to run (authentication, password change, ...) and what the expected error code is, so that both positive and negative tests are supported. The array of pam_testcase structures is then passed to a function called run_pamtest() that executes them with the help of a default conversation function provided by libpamtest. If the test requires a custom conversation function, another test driver called run_pamtest_conv() is also available that allows developers to supply their own conversation function.
The default conversation function provided by libpamtest allows the programmer to supply conversation input (typically a password) and also a string array that would capture any output that the conversation emits during the PAM transaction. As an example, the following test calls the PAM change password function, changes the password, and then verifies the new password by authenticating using the new password:
enum pamtest_err perr; const char *new_authtoks[] = { "secret" /* login with old password first */ "new_secret", /* provide a new password */ "new_secret", /* verify the new password */ "new_secret", /* login with the new password */ NULL, }; struct pamtest_conv_data conv_data = { .in_echo_off = new_authtoks, }; struct pam_testcase tests[] = { /* pam function to execute and expected return code */ pam_test(PAMTEST_CHAUTHTOK, PAM_SUCCESS), pam_test(PAMTEST_AUTHENTICATE, PAM_SUCCESS), }; perr = run_pamtest("matrix", /* PAM service */ "trinity", /* user logging in */ &conv_data, tests); /* conversation data and array of tests */
As you can see, the test is considerably shorter than a hand-written one would be. In addition, the test developer doesn't have to handle the conversation, or open and close the PAM handle. Everything is done behind the scenes.
If one of the PAM transaction steps failed (for example if the passwords didn't match the database), the perr return variable would indicate a test failure with value PAMTEST_ERR_CASE. The developer could then fetch the failed case using the pamtest_failed_case() function and examine the test case further.
In addition to the standard PAM actions like AUTHENTICATE or CHAUTHTOK, libpamtest also supports several custom actions that might be useful in tests. One is PAMTEST_GETENVLIST, which dumps the full PAM module environment into the test case's output data field. Another is PAMTEST_KEEPHANDLE, which prevents the PAM handle from being closed — the test could go and perform custom operations on the handle before closing it by calling pam_end().
Module stacking
Another aspect that is normally quite hard to test is module stacking. That is, testing that your module is able to read a password that is provided by another module that was executed earlier in the stack. This is a quite common setup especially for PAM modules that handle authenticating remote users. Since local users should take precedence, the password would be read by pam_unix first and passed down the stack if no local user could be authenticated. Conversely, your module might pass an authentication token on to the PAM stack for other modules (such as Gnome Keyring's PAM module) that come later in the stack.
Normally, handling these stack items is only allowed from the module context, not application context. Because the test runs in the application context, we had to develop a way to pass data between the two. So, in order to test the stacking, two simple modules called pam_set_items.so and pam_get_items.so were added.
The purpose of pam_set_items.so is to read environment variables with names corresponding to internal PAM module items and to put the data from the environment variables onto the stack. The pam_get_items.so module works in the opposite direction, reading the PAM module items and putting them into the environment for the application to read. Suppose you wanted to test that the pam_unix.so module is able to read a password from the stack and later pass it on. The PAM service file for such a test would look like this:
auth required /absolute/path/to/pam_set_items.so auth required pam_unix.so auth required /absolute/path/to/pam_get_items.so
The test itself would first set the auth token into the process environment with putenv(), run the test, and then make sure the token was put into the PAM environment by the module by calling pam_getenv(). It's very convenient to use libpamtest's PAMTEST_GETENVLIST test case to read the PAM environment:
pamtest_err perr; const char *new_authtoks[] = { "secret" /* password */ }; struct pamtest_conv_data conv_data = { .in_echo_off = new_authtoks, }; struct pam_testcase tests[] = { pam_test(PAMTEST_AUTHENTICATE, PAM_SUCCESS), pam_test(PAMTEST_GETENVLIST, PAM_SUCCESS), }; setenv("PAM_AUTHTOK", "secret"); perr = run_pamtest("matrix", "trinity", &conv_data, tests); /* * tests[1].case_out.envlist now contains list of key-value strings, * find PAM_AUTHTOK to see what the authtok is. */
Finally, because it is often inconvenient to write tests in a low-level programming language like C, we also developed Python bindings for libpamtest. Using the Python bindings, an authentication test might look like this:
def test_auth(self): neo_password = "secret" tc = pypamtest.TestCase(pypamtest.PAMTEST_AUTHENTICATE) res = pypamtest.run_pamtest("neo", "matrix_py", [tc], [ neo_password ])
Of course libpamtest can be used with or without pam_wrapper's preloading and custom PAM service location.
Where to go from here?
We hope that this tool is useful for those developers who have struggled testing their PAM modules and applications. The authors are looking forward to more projects that implement tests for PAM modules. We are also looking forward for feedback for the current API and usability of pam_wrapper.
At the moment, only Linux-PAM and OpenPAM (FreeBSD) are tested and supported by pam_wrapper. The code is maintained in Git on the Samba Git server. If you want to discuss pam_wrapper you can do that on the samba-technical mailing list. For discussions, you can also join #cwrap on irc.freenode.net.
Index entries for this article | |
---|---|
Security | Tools |
GuestArticles | Hrozek, Jakub |
GuestArticles | Schneider, Andreas |