Category Archives: OpenPAM

We can patch it for you wholesale

…but remembering costs extra.

Every once in a while, I come across a patch someone sent me, or which I developed in response to a bug report I received, but it’s been weeks or months and I can’t for the life of me remember where it came from, or what it’s for.

Case in point—I’m typing this on a laptop I haven’t used in over two months, and one of the first things I found when I powered it on and opened Chrome was a tab with the following patch:

diff --git a/lib/libpam/modules/pam_login_access/pam_login_access.c b/lib/libpam/modules/pam_login_access/pam_login_access.c
index 945d5eb..b365aee 100644
--- a/lib/libpam/modules/pam_login_access/pam_login_access.c
+++ b/lib/libpam/modules/pam_login_access/pam_login_access.c
@@ -79,20 +79,23 @@ pam_sm_acct_mgmt(pam_handle_t *pamh, int flags __unused,

        gethostname(hostname, sizeof hostname);

-       if (rhost == NULL || *(const char *)rhost == '') {
+       if (tty != NULL && *(const char *)tty != '') {
                PAM_LOG("Checking login.access for user %s on tty %s",
                    (const char *)user, (const char *)tty);
                if (login_access(user, tty) != 0)
                        return (PAM_SUCCESS);
                PAM_VERBOSE_ERROR("%s is not allowed to log in on %s",
                    user, tty);
-       } else {
+       } else if (rhost != NULL && *(const char *)rhost != '') {
                PAM_LOG("Checking login.access for user %s from host %s",
                    (const char *)user, (const char *)rhost);
                if (login_access(user, rhost) != 0)
                        return (PAM_SUCCESS);
                PAM_VERBOSE_ERROR("%s is not allowed to log in from %s",
                    user, rhost);
+       } else {
+               PAM_VERBOSE_ERROR("neither host nor tty is set");
+               return (PAM_SUCCESS);
        }

        return (PAM_AUTH_ERR);

The patch fixes a long-standing bug in pam_login_access(8) (the code assumes that either PAM_TTY or PAM_RHOST is defined, and crashes if they are both NULL), but I only have the vaguest recollection of the conversation that led up to it. If you’re the author, please contact me so I can give proper credit when I commit it.

On testing, part III

I just got word of an embarrassing bug in OpenPAM Nummularia. The is_upper() macro, which is supposed to evaluate to true if its argument is an upper-case letter in the ASCII character set, only evaluates to true for the letter A:

#define is_upper(ch)                            \
        (ch >= 'A' && ch <= 'A')

This macro is never used directly, but it is referenced by is_letter(), which is referenced by is_pfcs(), which is used to validate paths and path-like strings, i.e. service names and module names or paths. As a consequence, OpenPAM does not support services or modules which contain an upper-case letter other than A. I never noticed because a) none of the services or modules in use on the systems I use to develop and test OpenPAM have upper-case letters in their names and b) there are no unit or regression tests for the character classification macros, nor for any code path that uses them (except openpam_readword(), which uses is_lws() and is_ws()).

The obvious course of action is to add unit tests for the character classification macros (r760) and then fix the bug (r761). In this case, complete coverage is easy to achieve since there are only 256 possible inputs for each predicate.

I have merged the fix to FreeBSD head (r262529 and r262530). Impatient users can fix their system by running the following commands:

% cd /usr/src/contrib/openpam
% svn diff -r758:762 svn://svn.openpam.org/openpam/trunk | patch
% cd /usr/src/lib/libpam/libpam
% make && make install

Unsurprisingly, writing more unit tests for OpenPAM is moving up on my TODO list. Please contact me if you have the time and inclination to help out.

Challenges in Identity Management and Authentication

This was my presentation at the 2012 EuroBSDCon in Warsaw, Poland. I’ve been meaning to write more extensively on this subject, but never got around to it. I just watched through the video twice, and it was a lot less cringe-inducing than I expected (especially when you consider that I was sick and sleep-deprived when I gave it).

Towards the end, I got a question about Apple’s security framework. In my answer, I referred to it as CDDL. That was a slip of the tongue; I was referring to CDSA, which is actually an Open Group specification which Apple implemented and open-sourced. Furthermore, CDSA does not to everything I said it does. However, Apple built their Security Services Framework (described in their Authentication, Authorization and Permissions Guide and various other documents) on top of CDSA; so the combination of CDSA and what Apple added on top does everything from key management to authentication and authorization.

My presentation at the 2013 EuroBSDCon in St Julians, Malta will continue where I left off last year, outlining a concrete solution based on the principles set forth in the second part of last year’s presentation (starting at 32:06).

pkgng without ports: addenda

Two things I forgot to mention in my previous post:

  1. In order to use OpenPAM from svn instead of the version that comes with FreeBSD, you need to copy security/pam_mod_misc.h and pam_debug_log.c into the OpenPAM source tree and adjust the Makefiles accordingly, otherwise FreeBSD’s service modules won’t run and you won’t be able to log in. I don’t plan to include this code in OpenPAM; I’d rather overhaul FreeBSD’s modules so they no longer need it.
  2. What I actually wanted to do, but didn’t because I needed a solution there and then, was patch automake itself to add a pkgng target so gmake pkgng creates a package with no additional input required (except possibly a +DESC file).

Creating pkgng packages without ports

Lately, I’ve been working on expanding the scope of OpenPAM to more than just a PAM library. Specifically, I’ve added support (in a separate library) for the OATH HOTP and TOTP one-time password algorithms. In the long term, I also intend to implement PSKC and OCRA, the ultimate goal being full compliance with the OATH client and server certification profiles. Part of the reason I’m doing this is that my employer needs it, which is why the University of Oslo holds the copyright on most of the OATH code, but it is also something I’ve been wanting to do for a long time, and which I believe will greatly benefit FreeBSD.

This is a large undertaking, though. I’m not comfortable rolling a new OpenPAM release with the OATH code at this time—and I probably won’t be for quite a while. I’ve created a “nooath” branch and may roll a release from that branch in order to get the many other OpenPAM improvements into FreeBSD 10.0, but that’s a different story.

In the meantime, I need a way to test my code; not just on a development machine, but also on semi-production systems such as my desktop and my home router. Once it’s tested, I also need a way to deploy it on mission-critical systems. All these systems have one thing in common: they are binary installations, maintained with freebsd-update rather than built from source. So I need a way to install a newer version of OpenPAM without disturbing the base version.

The easy answer is to install in /usr/local:

# ./configure --prefix=/usr/local
# gmake
# gmake install

We also need to make sure that everything that uses PAM uses the new version (which is 100% backward compatible with older applications and modules). Conveniently, for historical reasons, OpenPAM installs libpam.so.2, whereas FreeBSD 8.x and newer install libpam.so.5, so it’s a simple matter of mapping one to the other:

# echo "libpam.so.5 libpam.so.2" >>/etc/libmap.conf

That doesn’t address deployment, though. I don’t want to have to compile OpenPAM on every machine, and I already have a mechanism for distributing and updating software across multiple machines: my two pkgng repositories. Let’s take advantage of them by creating pkgng packages for OpenPAM.

I could create an OpenPAM port and build packages from there. There is even precedent for creating a port that obtains sources directly from a repository rather than from a release tarball, so I could test individual revisions. I would however need a copy of the ports tree—it used to be possible to build a port independently of the ports tree, but that time is long gone. Another drawback is that I would have to jump through hoops to create packages from a modified source tree (for pre-commit testing). Finally, I would not be able to create a package of a specific version without first installing that version locally. So creating a port is a less-than-ideal solution.

What I did instead was write a script which installs OpenPAM into a temporary directory and creates the package from there. Well—nearly: there is a small hitch due to a bug in pkg which I expect will be fixed in the near future.

Let’s take a look at some of the juicier parts of the script.

First, we need to determine the package name and version. The name is taken directly from configure.ac, and so is the version—at first. The thing is, I’d rather not have to continuously update configure.ac, so @PACKAGE_VERSION@ is normally “trunk” (or “nooath”) until I roll a release. Therefore, if @PACKAGE_VERSION@ is a word rather than a number, I use Subversion’s svnversion utility to retrieve the current revision number. If I can successfully extract a number from the ouput, I append it to the original value.

package="@PACKAGE@"
version="@PACKAGE_VERSION@"
if ! expr "$version" : "[0-9]{1,}$" >/dev/null ; then
    svnversion="$(svnversion 2>&1)"
    svnversion=$(expr "$svnversion" : '\([0-9][0-9]*\)[A-Z]\{0,1\}$')
    if [ -n "$svnversion" ] ; then
        version="$version-r${svnversion}"
    fi
fi

For reasons which will become clear later, we also need to know which version of pkg is installed, as well as the ABI:

pkgver=$(pkg query %v pkg)
[ -n "$pkgver" ] || error "Unable to determine pkgng version."
pkgabi=$(pkg -vv | awk '$1 == "ABI:" { print $2 }')
[ -n "$pkgabi" ] || error "Unable to determine package ABI."

Next, we create a temporary directory into which we will install the software, so we can create a package without touching the host system. The traps ensure that the temporary directory is deleted when the script exits or is interrupted (SIGINT). Two separate traps are needed, because if we install the same trap for both EXIT and INT, it will run twice in the SIGINT case: once due to the SIGINT itself and once because the script exits. Clearing the trap from within the trap handler doesn’t work, because traps are local to the block in which they were set.

info "Creating the temporary directory."
tmproot=$(mktemp -d "${TMPDIR:-/tmp}/$package-$version.XXXXXX")
[ -n "$tmproot" -a -d "$tmproot" ] || \
    error "Unable to create the temporary directory."
trap "exit 1" INT
trap "info Deleting the temporary directory. ; rm -rf '$tmproot'" EXIT
set -e

We can now install our software into the temporary directory ($make evaluates to either make or gmake with a few options to reduce the amount of noise GNU make generates):

info "Installing into the temporary directory."
$make install DESTDIR="$tmproot"

We need a manifest for the package. Most of it can be automatically generated from the information provided in configure.ac; the only hardcoded OpenPAM-specific information in my script are the comment and description. The latter can easily be avoided: if no description is provided, pkg create will look for a +DESC file in the same directory as the manifest and use its contents instead. The former is not so easily avoided; there is no autoconf macro for a short description of the package, and while PACKAGE_COMMENT="foo"; AC_SUBST(PACKAGE_COMMENT) would work, it feels sort of dirty. I’ll probably end up writing a custom macro that does just that.

Anyway, we start out with a stub:

info "Generating the stub manifest."
manifest="$tmproot/+MANIFEST"
cat >"$manifest" <<EOF
name: $package
version: $version
origin: local/$package
comment: [...]
arch: $pkgabi
www: @PACKAGE_URL@
maintainer: @PACKAGE_BUGREPORT@
prefix: @prefix@
desc:
  [...]
categories: local, security
EOF

The rest of the manifest consists of a list of files to be included in the package. We generate it automatically from the contents of our temporary directory, which shouldn’t contain anything that we don’t want to include.

info "Generating the file list."
(
    echo "files:"
    find "$tmproot" -type f | while read file ; do
        [ "$file" = "$manifest" ] && continue
        mode=$(stat -f%p "$file" | cut -c 3-)
        file="${file#$tmproot}"
        echo "  $file: { uname: root, gname: wheel, perm: $mode }"
    done
)>>"$manifest"

We hardcode the ownership as root:wheel, which is correct 99% of the time. This allows us to run the entire package creation process as an unprivileged user. Or it would, except that pkg contains some rather advanced logic to determine whether a package installs shared libraries or depends on shared libraries provided other packages, and that logic doesn’t take into account the case where the package root is not /. Setting LD_LIBRARY_PATH doesn’t help, since pkg doesn’t use the run-time linker, but reads and interprets the Elf headers itself. I haven’t yet managed to untangle that logic to the point where I can figure out where to insert the package root so it will find the correct libraries. The only workaround is to install the package to /, which requires root privileges.

info "Packaging."
if [ "$pkgver" \< "1.1.5" ] ; then
    info "pkg 1.1.4 or older detected."
    yesno "We must now install to /.  Proceed?" || error "Chicken."
    $make install
    pkg create -m "$tmproot" -o "$builddir"
else
    pkg create -r "$tmproot" -m "$tmproot" -o "$builddir"
fi

Note that I have optimistically predicted that the bug will be fixed in pkg 1.1.5…

This script should be easily adaptable to any other project that uses GNU autotools and doesn’t deviate too far from automake’s standard installation procedure. As previously mentioned, the only part of the script specific to OpenPAM is the stub manifest, and that can easily be changed.

On testing, part II

About a year ago, I blogged about writing unit tests for OpenPAM to weed out bugs in the configuration file parser, and mentioned a specific bug which my unit tests caught, and which I wouldn’t have noticed without them.

Yesterday, this came back to bite me.

The patch in question fixed the following edge case:

hello "" world

This is supposed to be parsed as three words (“hello

On testing

Last fall, I wrote a completely new configuration parser for OpenPAM Lycopsida. Although the new parser was far more robust than the one it replaced, it was large, unwieldy, and suffered from a number of issues relating to whitespace handling, which stemmed from reusing some old code which unfortunately was thoroughly documented and therefore could not be easily modified. So I decided to rewrite it again, from scratch this time.

Then I did what I should have done last fall but didn’t: I wrote some unit tests. And of the first dozen or so tests I came up with, three failed, revealing two different bugs—one of them fairly serious.

There’s a lesson in here somewhere…