[Exim] CRAM-MD5 with sasldb2

Top Page
Delete this message
Reply to this message
Author: Pat Lashley
Date:  
To: Exim Users Mailing List
Subject: [Exim] CRAM-MD5 with sasldb2
Ok, there's probably a pretty limited audience for this particular hack;
but I thought I'd post it, if only as an example of how powerful and
flexable Exim is.

First, the setup. I needed to upgrade a system that had been running
older versions of Cyrus and Exim. The authentication info was kept in
the sasldb; and a pam_sasldb.so had been written to allow Exim to use
it for PLAIN authentication. The goal was to move to the latest versions
of Cyrus and Exim, and to add CRAM-MD5 authentication to Exim.

The good news is that Cyrus SASL v2 has switched from per-method hashed
authentication tokens to a single plain-text UserPassword entry in the
new sasldb2; and that applications linking to libsasl2 can still use the
old-format individual entries. More good news is that sasldb2 is just
a Berkeley DB file; so it is accessable to Exim for any type of
authenticator.
(Given proper file permissions and Exim uid/gid.)

The bad news is that Exim needs the plaintext password and there doesn't
seem to be any sort of auto-transition that will convert the old entries
to a new entry on successful plaintext login. (And, of course, I didn't
have the passwords for the individual mailbox users...)

Further bad news is that the keys in the sasldb(2) contain embedded
NULs and Exim doesn't seem to handle strings with embedded NULs.

So, I basicly configured Exim to use perl to directly access sasldb2;
and to do PLAIN or LOGIN authentication with either the new cleartext
UserPassword or the old hashed cmusaslsecretPLAIN entry. AND I added
code to update the sasldb2 info if the cmusaslsecretPLAIN version had
been successfully matched. So after each user one successful PLAIN
authentication, they can then switch to CRAM-MD5. Here are the relevant
sections of the config file. (The perl chunk goes in the main config
section.)



-Pat


#
---------------------------------------------------------------------------
#   We want to share authentication info that is already in the Cyrus
#   SASL database.  Unfortunately, the Cyrus-SASL library uses embedded
#   NULs and Exim can't handle them.  So we need some perl hackery.
#
#   The realm parameter is optional for these functions.  If not specified,
#   the value of $primary_hostname will be used.
#
#   getPw(userid [, realm])
#       If there is a userPassword key for the given user and realm,
#       return it; otherwise return undef.  This function is suitable
#       for use with authenticators that require a plaintext password
#       for construction of a challenge-response dialog.  (E.g., cram-md5)
#
#   checkPw(userid, password [, realm])
#       First retrieves the userPassword value for the given userid and
#       realm.  If one exists, it compares it against the given password
#       and returns the value of the comparison.
#
#       If there was no userPassword, it retrieves the cmusaslsecretPLAIN
#       value for the given userid and realm.  If there is none, returns
#       undef.  Otherwise uses the salt from the value to hash the password.
#
#       If $autoTransition is true, it compares the hashed password with
#       the retrieved value. If they match, it creates a userPasword key
#       in the sasldb for the given userid and realm; and sets its value
#       to the given password; then it deletes the cmusaslsecret* keys
#       and returns true.  If they did not match, it returns false.
#
#       If $autoTransition is false, compare the hashed password with the
#       retrieved value and return the result of the comparison.
#
#       NOTE: Auto-transition requires that the exim user have write
#       access to the sasldb.  Without auto-transition, read access
#       is sufficient.
#
#   makeKey(userid, keyid [, realm])
#       This function is intended for internal use by the other two
functions.
#       Given a userid, an keyid, and an optional realm, it constructs and
#       returns the old-style sasldb entry key.
#
#
#   If you do not want the auto-transition capability, you can add the
#   following line to the tie expression and remove the 'if ( $ret && ...'
#   block.
#               -Flags    => DB_RDONLY
#
perl_startup    = \
        use BerkeleyDB ; \
        my $autoTransition      = 0 ; \
        my $defaultRealm        = Exim::expand_string('$primary_hostname')
; \
        my %sdb ; \
        my $Sdb = tie %sdb, "BerkeleyDB::Hash", \
                -Filename => "/usr/local/etc/sasldb2" \
            or die "Could not tie to /usr/local/etc/sasldb2: $!\n" ; \
        sub makeKey ($$;$) { \
            my ($usr, $key, $realm) = @_ ; \
            $realm = $defaultRealm      unless $realm ; \
            return "${usr}\000${realm}\000${key}" ; \
        } \
        sub getPw   ($;$) { \
            return $sdb{makeKey(shift, 'userPassword', shift)} ; \
        } \
        sub checkPw ($$;$) { \
            use Digest::MD5 ; $m = Digest::MD5->new ; \
            my ($usr, $val, $realm) = @_ ; \
            my $u = makeKey($usr, 'userPassword', $realm) ; \
            return ($sdb{$u} eq $val) if  exists $sdb{$u} ; \
            my $p = makeKey($usr, 'cmusaslsecretPLAIN', $realm) ; \
            my $V = $sdb{$p} ; return undef unless $V ; \
            my ($s,$h)=unpack('a16 x a16', $V) ; \
            my $ret = $h eq  $m->add($s, 'sasldb', $val)->digest ; \
            if  ( $ret && $autoTransition ) { \
                $sdb{$u} = $val ;       delete $sdb{$p} ; \
                delete $sdb{makeKey($usr, 'cmusaslsecretCRAM-MD5',
$realm)};\
                delete $sdb{makeKey($usr, 'cmusaslsecretDIGEST-MD5',
$realm)};\
                $Sdb->db_sync() ; \
            } \
            return $ret ; \
        }







begin authenticators
# We want to use the same user<=>password bindings that we use for the
# Cyrus IMAP4/POP3 connections. Exim can now be built to use the
# saslauthd; but since Cyrus SASL v2 now stores the passwords in
# the clear, and sasldb2 is a standard Berkeley DB file; we should
# be able to directly access it for use in and CRAM-MD5.
#
# *** NOTE: sasldb2 files created by converting the old sasldb files
# *** will still have the old-style hashed per-mechanism entries
# *** instead of the new shared cleartext version. (At least until
# *** the password is updated.)


# We want to use the same user<=>password bindings that we use for the
# Cyrus IMAP4/POP3 connections. Since Exim doesn't use libsasl, the
# only way we can do that is by comparing against the plaintext password
# in the sasldb via our pam_sasldb module.


#   The PLAIN authentication mechanism (RFC 2595) specifies that three
#   strings be sent with the AUTH command. The second and third of them
#   are a user/password pair.
plain:
    driver              = plaintext
    public_name         = PLAIN
    #   We should be able to do a dbm lookup in the sasldb2 database using
    #   a key composed by concatenating the username, domain name, and
    #   'userPassword'.  BUT cyrus-sasl puts NULs between the components
    #   and exim can't handle strings with embedded NULs...  Hence the perl.
    server_condition    = ${perl {checkPw} {$2} {$3} {gate.DOMAIN}}
    server_set_id       = $2


#   The LOGIN mechanism is not a standard but is used in many programs.
#   It doesn't provide any info on the AUTH command; but responds to
#   prompts.  Some clients are reputedly -very- sensitive to exact spelling
#   of the prompts.  (Unsurprisingly, Outlook Express is reported to be
#   among them.)
login:
    driver              = plaintext
    public_name         = LOGIN
    server_prompts      = Username : Password
    server_condition    = ${perl {getPw} {$1} {gate.DOMAIN}}
    server_set_id       = $1


#   The CRAM-MD5 mechanism avoids sending the password in the clear by
#   computing an MD5 digest from the password and session-specific info.
#   The authenticator puts the username in $1 and expands session_secret
#   to obtain a plain-text password; which it then runs through the
#   CRAM-MD5 algorythm to obtain a value to be compared against what
#   the client sent
cram:
    driver              = cram_md5
    public_name         = CRAM-MD5
    server_secret       = ${perl {getPw} {$1} {gate.DOMAIN}}
    server_set_id       = $1


#-------------------------------------------------------------------------