The SMTP-Auth in the current version of QPSMTP (v0.28) is broken and
wrong. I have fixed it and attached a diff.
Problem 1: The AUTH PLAIN method
This method is described in RFC2554 and RFC2595. The client forms a string
of the form "<NUL>username<NUL>password" and encodes this string using
base64. The command issued then is 'AUTH PLAIN <base64string>'. For example
with the username test and 1234 the base64string can be determined (for
testing) as in the following example:
perl -MMIME::Base64 -e 'print encode_base64("\0"."test"."\0"."1234");'
AHRlc3QAMTIzNA==
The command issued now would be
AUTH PLAIN AHRlc3QAMTIzNA==
The current implementation of SMTP AUTH in qpsmtpd does in fact support this
notation. However, although not specified, many clients seem to do the PLAIN
authentication in two steps, first issueing a "AUTH PLAIN" command without
the base64 string parameter, and after beeing invited by the server sends
the base64 string. I show an example using the german mail provider GMX:
220 {mp024} GMX Mailservices ESMTP
EHLO itsme
250-{mp024} GMX Mailservices
250-8BITMIME
250-ENHANCEDSTATUSCODES
250-AUTH=LOGIN CRAM-MD5 PLAIN
250-AUTH CRAM-MD5 LOGIN PLAIN
250 STARTTLS
AUTH PLAIN
334 Go on
<base64 string here>
Now qpsmtpd has basic provisions for cases where the base64 string was
missing. But instead of accepting one base64 encoded string, it will ask
for two cleartext strings (username and password) which is just wrong.
Here is an excerpt from the current (wrong) cvs code:
if ( $mechanism eq "plain" ) {
if ($prekey) {
( $passHash, $user, $passClear ) = split /\x0/,
decode_base64($prekey);
}
else {
$session->respond( 334, "Username:" );
# We read the username and password from STDIN
$user = <>;
[...]
$session->respond( 334, "Password:" );
$passClear = <>;
So instead of accepting one base64 string the code will await two cleartext
strings which will just not work with existing mailclients. The code need to
be modified as in my following fixed code excerpt:
if ( $mechanism eq "plain" ) {
if (!$prekey) {
$session->respond( 334, "Please continue" );
$prekey= <>;
}
( $passHash, $user, $passClear ) = split /\x0/,
decode_base64($prekey);
This code is even dramatically shorter and easier than the provided one, and
it works with existing mail clients (like TheBat).
Problem 2: The AUTH LOGIN method
The supporting code for the LOGIN-Method is commented out in current CVS
implementation. Problem is, that a plugin can register a LOGIN-Handler with
$self->register_hook( "auth-login", "authcpw");
and qpsmtpd will happily announce beeing capable of AUTH LOGIN while the
supporting code is defunct. So this code need to be fixed. Thankfully the
existing code is nearly correct with one problem. Let me first take a look
on how AUTH LOGIN works (not specified in any RFC):
Client issues the 'AUTH LOGIN' command. Now the Server will respond with 334
and a base64 encoded 'Username:' prompt, accept a base64 encoded response
and will continue with a base64 encoded 'Password:' prompt, again accepting
an base64 encoded response.
Let me show you an excerpt from the current CVS code:
# $session->respond(334, encode_base64("User Name:"));
# $user = decode_base64(<>);
[...]
# $session->respond(334, encode_base64("Password:"));
# $passClear = <>;
# $passClear = decode_base64($passClear);
This code is nearly correct, although i'm not sure why in the second case
the code is not '$passClear = decode_base64(<>)'. But the problem lies
within the called 'encode_base64' method for the responses. This method will
add a newline after the encoded base64 string, causing an additional (wrong)
newline output on the network connection.
Therefore we need to define a wrapper function here, removing the newline,
which get called instead:
sub e64
{
my ($arg) = @_;
my $res = encode_base64($arg);
chomp($res);
return($res);
}
So the existing code now will be:
$session->respond(334, &e64("Username:"));
[...]
$session->respond(334, &e64("Password:"));
Solution:
I have corrected the described problems and tested the code both manually by
issueing the apropiate commands as well as with real life clients like
TheBat. I'm certain, that the code now is correct for both AUTH LOGIN and
AUTH PLAIN, haven't checked the MD5 code yet.
The diff is attached.
-kju
--
It's an insane world, but i'm proud to be a part of it. -- Bill Hicks
diff -Naur qpsmtpd.cvs/lib/Qpsmtpd/Auth.pm installed/lib/Qpsmtpd/Auth.pm
--- qpsmtpd.cvs/lib/Qpsmtpd/Auth.pm 2004-07-05 11:24:59.000000000 +0200
+++ installed/lib/Qpsmtpd/Auth.pm 2004-07-29 12:58:27.000000000 +0200
@@ -214,6 +214,14 @@
use Qpsmtpd::Constants;
use MIME::Base64;
+sub e64
+{
+ my ($arg) = @_;
+ my $res = encode_base64($arg);
+ chomp($res);
+ return($res);
+}
+
sub Qpsmtpd::SMTP::auth {
my ( $self, $arg, @stuff ) = @_;
@@ -235,59 +243,38 @@
$mechanism = lc($mechanism);
if ( $mechanism eq "plain" ) {
- if ($prekey) {
- ( $passHash, $user, $passClear ) = split /\x0/,
- decode_base64($prekey);
+ if (!$prekey) {
+ $session->respond( 334, "Please continue" );
+ $prekey= <>;
}
- else {
+ ( $passHash, $user, $passClear ) = split /\x0/,
+ decode_base64($prekey);
- $session->respond( 334, "Username:" );
+ } elsif ($mechanism eq "login") {
- # We read the username and password from STDIN
- $user = <>;
- chop($user);
- chop($user);
- if ( $user eq '*' ) {
- $session->respond( 501, "Authentification canceled" );
- return DECLINED;
- }
-
- $session->respond( 334, "Password:" );
- $passClear = <>;
- chop($passClear);
- chop($passClear);
- if ( $passClear eq '*' ) {
- $session->respond( 501, "Authentification canceled" );
- return DECLINED;
- }
+ if ( $prekey ) {
+ ($passHash, $user, $passClear) = split /\x0/, decode_base64($prekey);
}
-
- }
-
- # elsif ($mechanism eq "login") {
- # if ( $prekey ) {
- # ($passHash, $user, $passClear) = split /\x0/, decode_base64($prekey);
- # }
- # else {
- #
- # $session->respond(334, encode_base64("User Name:"));
- # $user = decode_base64(<>);
- # #warn("Debug: User: '$user'");
- # if ($user eq '*') {
- # $session->respond(501, "Authentification canceled");
- # return DECLINED;
- # }
- #
- # $session->respond(334, encode_base64("Password:"));
- # $passClear = <>;
- # $passClear = decode_base64($passClear);
- # #warn("Debug: Pass: '$pass'");
- # if ($passClear eq '*') {
- # $session->respond(501, "Authentification canceled");
- # return DECLINED;
- # }
- # }
- # }
+ else {
+
+ $session->respond(334, &e64("Username:"));
+ $user = decode_base64(<>);
+ #warn("Debug: User: '$user'");
+ if ($user eq '*') {
+ $session->respond(501, "Authentification canceled");
+ return DECLINED;
+ }
+
+ $session->respond(334, &e64("Password:"));
+ $passClear = <>;
+ $passClear = decode_base64($passClear);
+ #warn("Debug: Pass: '$pass'");
+ if ($passClear eq '*') {
+ $session->respond(501, "Authentification canceled");
+ return DECLINED;
+ }
+ }
+ }
elsif ( $mechanism eq "cram-md5" ) {
# rand() is not cryptographic, but we only need to generate a globally