AUTH CRAM-MD5

By Susam Pal on 07 Nov 2011

Introduction

Last night, while I was setting up my SMTP server, I decided to dig deeper into CRAM-MD5 authentication mechanism. It is a challenge-response authentication mechanism and involves HMAC-MD5. We don't use SSL/TLS in the SMTP session examples below in order to show the underlying protocol in clear. In practice, any email program should be configured to use SSL/TLS while having a session with an SMTP server. We will first see a few examples of other authentication mechanisms before discussing the CRAM-MD5 mechanism.

AUTH PLAIN

Here is an example of a session that uses the PLAIN authentication mechanism:

$ telnet susam.in 25
Trying 106.187.41.241...
Connected to susam.in.
Escape character is '^]'.
220 tesseract.susam.in ESMTP Exim 4.72 Mon, 07 Nov 2011 20:27:56 +0530
EHLO nifty.localdomain
250-tesseract.susam.in Hello nifty.localdomain [122.167.80.194]
250-SIZE 52428800
250-PIPELINING
250-AUTH PLAIN LOGIN CRAM-MD5
250-STARTTLS
250 HELP
AUTH PLAIN AGFsaWNlAHdvbmRlcmxhbmQ=
235 Authentication succeeded
MAIL FROM:<alice@susam.in>
250 OK
RCPT TO:<example.recipient@gmail.com>
250 Accepted
DATA
354 Enter message, ending with "." on a line by itself
Date: Mon, 07 Nov 2011 20:28:00 +0530
From: Alice <alice@susam.in>
To: Example Recipient <example.recipient@gmail.com>
Subject: Test email

This is a test email.
.
250 OK id=1RNQef-0004e7-7s
QUIT
221 tesseract.susam.in closing connection
Connection closed by foreign host.

The string "AGFsaWNlAHdvbmRlcmxhbmQ=" in the AUTH PLAIN command is the the base64 encoding of the string "\0alice\0wonderland" where "\0" indicates a null character, "alice" is the sender's user name and "wonderland" is the sender's password. If an eavesdropper intercepts this traffic, he or she can easily find the user's password by simply decoding the base64 response sent by the client. Here is an example of decoding the base64 response with Python 2.7:

>>> 'AGFsaWNlAHdvbmRlcmxhbmQ='.decode('base64')
'\x00alice\x00wonderland'

This is also susceptible to replay attacks as the eavesdropper can use the AUTH PLAIN line containing the base64 encoded credentials to log into the server in future. This is why it is very important to secure the connection with SSL/TLS while having a session with the SMTP server.

AUTH LOGIN

Here is another example snippet that shows the LOGIN mechanism:

AUTH LOGIN
334 VXNlcm5hbWU6
YWxpY2U=
334 UGFzc3dvcmQ6
d29uZGVybGFuZA==
235 Authentication succeeded

Here are the base64 responses decoded with Python 2.7:

>>> 'VXNlcm5hbWU6'.decode('base64')
'Username:'
>>> 'YWxpY2U='.decode('base64')
'alice'
>>> 'UGFzc3dvcmQ6'.decode('base64')
'Password:'
>>> 'd29uZGVybGFuZA=='.decode('base64')
'wonderland'

If the session isn't encrypted, LOGIN authentication mechanism is susceptible to the same problems that PLAIN authentication mechanism is susceptible to.

AUTH CRAM-MD5

Let us take a look at the CRAM-MD5 authentication mechanism now. When the client selects the CRAM-MD5 authentication mechanism, the server sends a base64 encoded challenge like this:

AUTH CRAM-MD5
334 PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg==

An HMAC is calculated for this challenge with the password as the key and MD5 as the hash function. A string is formed by concatenating the user name, a space and the hexadecimal representation of the HMAC. The base64 encoding of this string is sent as the response by the client. The following statements in Python 2.7 show how a response can be formed for the above challenge:

>>> 'PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg=='.decode('base64')
'<17893.1320679123@tesseract.susam.in>'
>>> import hmac, hashlib
>>> hmac.new('wonderland', '<17893.1320679123@tesseract.susam.in>', hashlib.md5).hexdigest()
'64b2a43c1f6ed6806a980914e23e75f0'
>>> 'alice 64b2a43c1f6ed6806a980914e23e75f0'.encode('base64')
'YWxpY2UgNjRiMmE0M2MxZjZlZDY4MDZhOTgwOTE0ZTIzZTc1ZjA=\n'

Of course, this can be written as a small function:

import hmac, hashlib
def cram_md5_response(username, password, base64challenge):
    return (username + ' ' +
            hmac.new(password,
                     base64challenge.decode('base64'),
                     hashlib.md5).hexdigest()).encode('base64')

The following snippet shows the SMTP server accepting the client-response:

AUTH CRAM-MD5
334 PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg==
YWxpY2UgNjRiMmE0M2MxZjZlZDY4MDZhOTgwOTE0ZTIzZTc1ZjA=
235 Authentication succeeded

When the connection is not secured, CRAM-MD5 authentication mechanism is relatively more secure than the other two mechanisms because the password cannot be retrieved by decoding the base64 encoded response from the client. The password is used as the key to calculate the HMAC but the password itself is not present in the response. It prevents replay attacks too because the server sends an unpredictable challenge for every authentication. The response sent by the client for a certain challenge is invalid for another instance of authentication because the other instance would involve a different unpredictable challenge.

Further Reading

Here is a list of hyperlinks for further reading:

  1. RFC 4954: SMTP Service Extension for Authentication
  2. RFC 4616: The PLAIN Simple Authentication and Security Layer (SASL) Mechanism
  3. RFC 2195: IMAP/POP AUTHorize Extension for Simple Challenge/Response
  4. RFC 2104: HMAC: Keyed-Hashing for Message Authentication