[原创]fetchmail代码阅读笔记---ESMTP的认证方式

来源:互联网 发布:mysql怎么删除表字段 编辑:程序博客网 时间:2024/05/17 04:21
fetchmail代码阅读笔记---ESMTP的认证方式

作者: 默难 ( monnand@gmail.com )

0    引言
fetchmail是Eric S. Raymond组织编写的一款全功能的 IMAP 和 POP 客户程序. 它允许用户自动地从远程的 IMAP 和 POP 服务器上下载邮件, 并转发到指定邮箱中.
本文主要通过对其中的smtp.c文件中描述的ESMTP认证函数( SMTP_auth )进行分析, 并由此对ESMTP中的认证方式有一个大致的介绍.

1    概述

本文主要内容是分析fetchmail的代码, 而非详细叙述ESMTP的认证过程. 相关内容可以参看[1] [2] [3] 以及相关RFC.
fetchmail实现的ESMTP认证方式包括: CRAM-MD5 PLAIN和LOGIN三种. 本文所给出的代码不能单独编译. 本文所用的fetchmail版本为6.3.4

2    精简代码及其注释
这里首先把源代码中用到的一些重要函数的功能介绍一下, 它们的具体实现可以不用关心:

o SockPrintf
    函数原型: int SockPrintf(int sock, const char *format, ...) ;
    函数定义所在文件: $FETCHMAIL/socket.c( $FETCHMAIL表示fechtmail所在目录.下同 )
    函数功能: 根据format指定的格式, 向文件描述符sock写入字符串. 该函数的参数列表是可变长的. 具体使用方法与printf相同.
    函数返回值: 返回写入到sock的字节数.发生错误返回-1

o SockRead
    函数原型: int SockWrite(int sock, char *buf, int size);
    函数定义所在文件: $FETCHMAIL/socket.c
    函数功能: 从文件描述符sock读入最多size个字节的信息到buf起始的内存区域.
    函数返回值: 返回真正读入到buf的字节数. 发生错误返回-1

o from64tobits
    函数原型: int from64tobits(void *out_, const char *in, int maxlen);
    函数定义所在文件: $FETCHMAIL/base64.c
    函数功能: 利用base64解码算法, 将字符串in解码. 将解码后的信息存入out_指定的内存区域中. 最多写入maxlen个字节到out_指定的内存区域.存入到out_内的信息不以NULL结尾
    函数返回值: 返回实际写入到out_内的字节数. 发生错误返回-1

o to64frombits
    函数原型: void to64frombits(char *out, const void *in_, int inlen);
    函数定义所在文件: $FETCHMAIL/base64.c
    函数功能: 将指定信息进行base64编码. in_表示指定信息存储的内存区域的起始地址, inlen表示指定信息的字节数. out表示将指定信息编码后存储到的位置. 存入到out内的信息是以NULL结尾字符串
    函数返回值: 无

o hmac_md5
    函数原型: void hmac_md5 (char *password,  size_t pass_len,
         char *challenge, size_t chal_len, unsigned char *response, size_t resp_len);
    函数定义所在文件: $FETCHMAIL/cram.c
    函数功能: 将password和challenge按照hmac-md5[2]指定的方法计算出摘要, 并将摘要存储在response中. pass_len chal_len和resp_len分别表示password challenge和response的长度
    函数返回值: 无

o SMTP_ok
    函数原型: int SMTP_ok(int sock, char smtp_mode);
    函数定义所在文件: $FETCHMAIL/smtp.c
    函数功能: 读取SMTP服务器传来的状态信息. smtp_mode表示使用的协议是SMTP还是ESMTP.
    函数返回值: 返回当前服务器状态值

o report
    函数原型: void report (FILE *errfp, const char *message, ...);
    函数定义所在文件: $FETCHMAIL/report.c
    函数功能: 向指定文件写入信息. 主要用于程序报错
    函数返回值: 无

这里给出的代码是精简之后的代码. 删除了一些调试信息和原有的注释.

static void SMTP_auth(int sock, char smtp_mode, char *username, char *password, char *buf)
/* 函数参数说明:
 * sock: 用于通信的套接字
 * smtp_mode: 表示使用的协议是SMTP还是ESMTP. 可以忽略此参数
 * username: 用户名
 * passowrd: 密码
 * buf: 向服务器发送EHLO后服务器的返回信息. 用于判断采用的认证方式
 */
{    
    int c;
    char *p = 0;
    char b64buf[512];
    char tmp[512];
/* 变量声明 */

    if (!username || !password) return;
/* 非法参数 */

    memset(b64buf, 0, sizeof(b64buf));
    memset(tmp, 0, sizeof(tmp));
/* 临时变量初始化 */

    if (strstr(buf, "CRAM-MD5")) {
/* 采用CRAM-MD5的认证方式[3] */
        unsigned char digest[16];
    /* 临时变量. 用于存储hmac_md5计算出的摘要 */
        memset(digest, 0, sizeof(digest));

        SockPrintf(sock, "AUTH CRAM-MD5/r/n");
        SockRead(sock, smtp_response, sizeof(smtp_response) - 1);
        strncpy(tmp, smtp_response, sizeof(tmp));
    
        tmp[sizeof(tmp)-1] = '/0';
    /* 向服务器提出使用CRAM-MD5认证方式. 读取服务器的回复信息,
     *    并把回复信息存储到tmp中
     */
        if (strncmp(tmp, "334 ", 4)) {
            SMTP_auth_error(sock, GT_("Server rejected the AUTH command./n"));
            return;
        }
    /* 服务器返回状态码不等于334, 表示服务器端拒绝认证方式. 通过SMTP_auth_error函数报错并返回 */

        p = strchr(tmp, ' ');
        p++;
    /* 跳过服务器返回的状态码 */

        if (from64tobits(b64buf, p, sizeof(b64buf) - 1) <= 0) {
            SMTP_auth_error(sock, GT_("Bad base64 reply from server./n"));
            return;
        }
    /* 服务器发送来消息格式是:
     *    状态码 字符串CRLF
     *    状态码在前面p=strchr(tmp,' ');p++两句执行后已经被跳过
     *    CRLF代表"/r/n"
     *    此时p指向了一个字符串.
     *    该字符串是由服务器随机产生的, 并且已经经过base64编码
     *    from64tobits的作用就是对该字符串进行解码. 如果发生错误则报错
     */
        hmac_md5(password, strlen(password),
             b64buf, strlen(b64buf), digest, sizeof(digest));
    /* 将服务器返回的随机字符串和用户的密码通过hmac-md5[3]算法算出摘要. 并将摘要存储在digest中 */
        snprintf(tmp, sizeof(tmp),
        "%s %02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
        username,  digest[0], digest[1], digest[2], digest[3],
        digest[4], digest[5], digest[6], digest[7], digest[8],
        digest[9], digest[10], digest[11], digest[12], digest[13],
        digest[14], digest[15]);
    /* 此函数执行完后, tmp中则存储着用户名, 之后一个空格,
     *    然后是用16进制表示的摘要( 通过hmac-md5算法计算出的 ). 摘要用的16进制表示需使用小写字母.
     */

        to64frombits(b64buf, tmp, strlen(tmp));
        SockPrintf(sock, "%s/r/n", b64buf);
    /* 将tmp中存储的内容进行base64编码, 并传递给服务器端 */
        SMTP_ok(sock, smtp_mode);
    /* 通过SMTP_ok函数查看是否正常通过认证 */
    }
    else if (strstr(buf, "PLAIN")) {
/* 采用PLAIN认证方式 */
        int len;
        snprintf(tmp, sizeof(tmp), "^%s^%s", username, password);
    /* 将用户名和密码中间用'^'字符分割, 并将'^'字符至于用户名前 */
        len = strlen(tmp);
        for (c = len - 1; c >= 0; c--)
        {
            if (tmp[c] == '^')
                tmp[c] = '/0';
        }
    /* 将tmp中的'^'内容转换成'/0'. 换句话说, tmp将是以'/0'开头,
     *    而用户名和密码之间有一个'/0'
     */
        to64frombits(b64buf, tmp, len);
        SockPrintf(sock, "AUTH PLAIN %s/r/n", b64buf);
    /* 将tmp中存储的内容前加上"AUTH PLAIN "后进行base64编码, 并传递给服务器端 */
        SMTP_ok(sock, smtp_mode);
    /* 通过SMTP_ok函数查看是否正常通过认证 */
    }
    else if (strstr(buf, "LOGIN")) {
/* 采用LOGIN认证方式 */
        SockPrintf(sock, "AUTH LOGIN/r/n");
        SockRead(sock, smtp_response, sizeof(smtp_response) - 1);
        strncpy(tmp, smtp_response, sizeof(tmp));
        tmp[sizeof(tmp)-1] = '/0';
    /* 向服务器提出使用LOGIN认证方式. 读取服务器的回复信息,
     *    并把回复信息存储到tmp中
     */

        if (strncmp(tmp, "334 ", 4)) {
            SMTP_auth_error(sock, GT_("Server rejected the AUTH command./n"));
            return;
        }
    /* 服务器返回状态码不等于334, 表示服务器端拒绝认证方式. 通过SMTP_auth_error函数报错并返回 */

        p = strchr(tmp, ' ');
        p++;
    /* 跳过服务器返回的状态码 */

        if (from64tobits(b64buf, p, sizeof(b64buf) - 1) <= 0) {
            SMTP_auth_error(sock, GT_("Bad base64 reply from server./n"));
            return;
        }
    /* 服务器返回一个由base64编码的字符串.
     * 该字符串在之后的认证过程中不会用到.
     *  因此此处仅仅是查看该字符串的合法性, 而不必对其内容进行分析
     */
        to64frombits(b64buf, username, strlen(username));
        SockPrintf(sock, "%s/r/n", b64buf);
    /* 将用户名进行base64编码后发送给服务器.*/
        SockRead(sock, smtp_response, sizeof(smtp_response) - 1);
        strncpy(tmp, smtp_response, sizeof(tmp));
        tmp[sizeof(tmp)-1] = '/0';
    /* 读取服务器的回复信息, 并并把回复信息转存到tmp中 */
        p = strchr(tmp, ' ');
    /* 跳过服务器的状态码 */
        if (!p) {
            SMTP_auth_error(sock, GT_("Bad base64 reply from server./n"));
            return;
        }
    /* 服务器返回的格式非法, 程序报错 */
        p++;
        memset(b64buf, 0, sizeof(b64buf));
        if (from64tobits(b64buf, p, sizeof(b64buf) - 1) <= 0) {
            SMTP_auth_error(sock, GT_("Bad base64 reply from server./n"));
            return;
        }
    /* 服务器返回的信息格式是一个状态码加一个空格, 之后是一个由base64编码后的字符串
     * 同样, 这个字符串在下面的认证过程中不会被用到, 因此此处也仅仅是检查其合法性
     */
        to64frombits(b64buf, password, strlen(password));
        SockPrintf(sock, "%s/r/n", b64buf);
    /* 将密码进行base64编码后发送给服务器 */
        SMTP_ok(sock, smtp_mode);
    /* 通过SMTP_ok函数查看是否正常通过认证 */
    }
    return;
}


3    认证过程分析
fetchmail中实现了ESMTP的三个认证方式: CRAM-MD5 PLAIN和LOGIN. 下面对这三种认证方式的流程进行一个总结.

CRAM-MD5:
客户端首先向服务器端发送一个字符串: "AUTH[SPACE]CRAM-MD5[CRLF]".其中[SPACE]表示一个空格;[CRLF]表示回车换行符, 即"/r/n". 下同.
如果服务器拒绝认证方式, 则返回一个字符串: "[NUM][SPACE]str". 其中[NUM]为三位数字的服务器状态码( 下同 ).当状态码不等于334表示拒绝认证方式. str是一个服务器端定义的字符串, 用于描述错误.
如果服务器接受认证方式, 则返回一个字符串: "[NUM][SPACE]str_base64".其中str_base64是一个随机字符串经过base64编码后的字符串.
客户端收到服务器的信息后, 执行如下操作:
首先利用base64解码算法将str_base64解码. 解码后的字符串存入str
 *      call base64_decode
 *      input str_base64
 *      output str

之后利用hmac-md5算法计算出一个摘要digest
 *      call hmac_md5
 *      input password, str
 *      output digest

将摘要用小写字母的16进制表示, 并把字符串"username "与它合并, 成为字符串tmp
 *      string tmp = 'username digest'

将tmp进行base64编码
 *      call base64_encode
 *      input tmp
 *      output tmp_base64
最后, 客户端向服务器端发送字符串: "tmp_base64[CRLF]"
根据服务器的返回判断是否认证成功

PLAIN:
客户端首先做如下操作:
 *      string tmp = '^username^password'
 *      for each character in tmp
 *        tmp[i] = '/0' where tmp[i] == '^'
 *      call base64_encode
 *      input tmp
 *      output tmp_base64
最后, 客户端向服务器端发送字符串: "tmp_base64[CRLF]"
根据服务器的返回判断是否认证成功

LOGIN:
客户端首先向服务器端发送一个字符串: "AUTH[SPACE]LOGIN[CRLF].
如果服务器拒绝认证方式, 则返回一个字符串: "[NUM][SPACE]str". 其中[NUM]不等于334, 表示拒绝认证方式. str是一个服务器端定义的字符串, 用于描述错误.
如果服务器接受认证方式, 则返回一个字符串: "[NUM][SPACE]user_base64". 其中user_base64是一个利用base64编码后的字符串
客户端收到服务器信息后做如下操作:
 *      call base64_decode
 *      input user_base64
 *      output tmp
此处仅仅对服务器返回的字符串做合法性检测. 之后的认证过程中不会用到它

 *      call base64_encode
 *      input username
 *      output username_base64
接着, 客户端向服务器发送一个字符串: "username_base64"
服务器返回一个字符串:"[NUM][SPACE]pass_base64".其中pass_base64是一个利用base64编码后的字符串
客户端收到服务器信息后做如下操作:
 *      call base64_decode
 *      input pass_base64
 *      output tmp
此处也仅对服务器返回的字符串做合法性检测. 之后的认证过程中不会用到它

 *      call base64_encode
 *      input password
 *      output password_base64
最后, 客户端向服务器发送一个字符串: "password_base64"
根据服务器的返回判断是否认证成功

4    后记
本文对fetchmail中ESMTP认证部分的代码进行了分析. 由此粗略介绍了ESMTP的认证方式.
所提到的三种认证方式中, CRAM-MD5的安全性最强. 而其他两种认证方式个人认为与明文传输几乎没有区别. 因为base64本身就不是一个用于加密的算法.如果采用后两者的认证方式, 任何一个攻击者都可以轻易地通过简单的嗅探获得客户端与服务器端的通信, 最终获得用户的登录名和密码.

附录    参考文献
[1] J. Klensin, "Simple Mail Transfer Protocol", RFC 2821, April 2001.
[2] J. Klensin, R. Catoe, P. Krumviede, "IMAP/POP AUTHorize Extension for Simple Challenge/Response", RFC 2195 September 1997.
[3] Krawczyk, Bellare, Canetti, "HMAC: Keyed-Hashing for Message Authentication", RFC 2104, February 1997.