As the title suggests this is a quick guide to the authentication mechanism DIGEST-MD5 as used in XMPP. Digest authentication is used in other protocols also, but they are outside of the scope of this article.
Once an XMPP stream has been established and the DIGEST-MD5 SASL mechanism selected, the host should respond with a challenge that looks something like (when decoded from base-64):
realm="zoofware.com",nonce="0ddba11",qop="auth",charset=utf-8,algorithm=md5-sess
Now, the correct response would be:
username="jwb",realm="zoofware.com",nonce="0ddba11",cnonce="39931f36af066660f551
6142cbf02767",nc=00000001,qop=auth,digest-uri="xmpp/zoofware.com",charset=utf-8,
response=3353792a75fcb3337560e2a33041b3fa
Creating most of the response is incredibly simple, it’s simply a string composed of the following attributes:
username – The username part of the user’s JID (i.e jwb@zoofware.com)
realm – This should be the same realm as sent in the challenge, which should be the same as the domain part of the user’s JID.
nonce – Essentially an arbitrary string which is sent by the server in the challenge. It is for security.
cnonce – Another string, but this time generated by the client. It doesn’t matter what it is. Again for security.
nc – An incrementing counter to identify each request. Generally there is no need to change this from “00000001″.
qop – For XMPP, this will always be “auth”.
digest-uri – Simply the realm appended to the string “xmpp/”.
charset – The encoding to use.
The “response” directive is the hardest to generate; it is a hash based on several other hashes of passwords and so on and can be done in several steps. I have included my specific client C# code in the example.
The following example uses the following directives:
realm=”zoofware.com”
username=”jwb”
nonce=”0ddba11″
password=”secret”
cnonce=”randomz”
digest-uri=”xmpp/zoofware.com”
Step 1
Create a string of the form "username:realm:password".
Save this string as a byte array called A1.
byte[] A1 = ASCIIEncoding.UTF8.GetBytes(string.Format("{0}:{1}:{2}", Xmpp.UserName, Xmpp.UserDomain, Xmpp.Password));
Now compute the MD5 hash of A1 and keep the result in a byte array HA1_1.
MD5 md5 = MD5.Create();
byte[] HA1_1 = md5.ComputeHash(A1);
.
The result so far should be HA1_1 = afaab39bafa10040c5d165030c03e278.
Step 2
Create a string of the form ":nonce:cnonce" and save it as a byte array HA1_2.
byte[] HA1_2 = ASCIIEncoding.UTF8.GetBytes(string.Format(":{0}:{1}", elements["nonce"], cnonce));
.
Create a byte array HA1 which has a length equal to the length of HA1_1 + HA1_2. Then copy the cotents of HA1_1 and HA1_2 into the new array.
byte[] HA1 = new byte[HA1_2.Length + HA1_1.Length];
HA1_1.CopyTo(HA1, 0);
HA1_2.CopyTo(HA1, HA1_1.Length);
Compute the MD5 of HA1 and call it HA1.
HA1 = md5.ComputeHash(HA1);
.
The result so far should be HA1 = ef92f7f90f8bd0a364f01cfe9e9b0564.
Step 3
Create a byte array HA2 with the contents of the MD5 hash of the string “AUTHENTICATE:digest-uri”.
byte[] HA2 = md5.ComputeHash(ASCIIEncoding.UTF8.GetBytes(string.Format("AUTHENTICATE:xmpp/{0}", Xmpp.UserDomain)));
HA2 = c51d568aff0c3f543bdfbba2a6010a75.
Step 4
Now create two strings sHA1 and sHA2. Their contents need to be the base 16 representation of HA1 and HA2.
Important: Do not just convert each byte to it’s ASCII letter. The string needs to be its literal value.
For example, the byte array { 0x1a, 0x2e, 0x3f } would become the string 1a2e3f.
public static string ConvertToHexString(byte[] inArray)
{
StringBuilder sb = new StringBuilder();
foreach (byte part in inArray)
{
sb.AppendFormat("{0:x2}", part);
}
return sb.ToString();
}
Then compute the MD5 hash of the string “sHA1:nonce:nc:cnonce:qop:sHA2″. Save the result as the byte array hash.
So far: hash = 6dd79d3dee313216e188bfc17b6254b5.
byte[] b = ASCIIEncoding.ASCII.GetBytes(string.Format("{0}:{1}:{2}:{3}:{4}:{5}", ConvertToHexString(HA1), elements["nonce"], "00000001", cnonce, elements["qop"], ConvertToHexString(HA2)));
byte[] hash = md5.ComputeHash(b);
Final Step
pheww!
The base 16 representation of hash can now be used as the value for the response directive.
Thus the final response will be:
username="jwb",realm="zoofware.com",nonce="0ddba11",cnonce="randomz",nc=00000001
,qop=auth,digest-uri="xmpp/zoofware.com",charset=utf-8,response=6dd79d3dee313216
e188bfc17b6254b5.
Note: I’m not experienced with DIGEST-MD5 at all, so this solution my be incomplete for some situations. It does however work with the XMPP hosts I’ve tried it with.
Further reading:
http://en.wikipedia.org/wiki/Digest_access_authentication
http://www.ietf.org/rfc/rfc2831.txt
http://web.archive.org/web/20050224191820/http://cataclysm.cx/wip/digest-md5-crash.html
http://www.charliedigital.com/CategoryView,category,XMPP.aspx