[Dovecot] CRAM-MD5 Password Generation Algorithm

Bill Cole dovecot-20061108 at billmail.scconsult.com
Sun Apr 13 23:12:41 EEST 2008


At 11:57 AM +0100 4/13/08, Douglas Willcocks  imposed structure on a 
stream of electrons, yielding:
>On Sun, 13 Apr 2008 01:39:44 -0400, Bill Cole
><dovecot-20061108 at billmail.scconsult.com> wrote:
>>  At 9:32 PM +0100 4/12/08, Douglas Willcocks  imposed structure on a
>>  stream of electrons, yielding:
>>  [...]
>>>I've read through the document, and I now understand where exactly the
>>>precomputed context sits in the whole picture, but I'm still unsure how
>>  to
>>>reproduce it without dovecotpw.
>>
>>  In looking a little closer at RFC1321 that defines MD5, I think I see
>>  the trick.  The stored contexts are not full MD5's of Ki and Ko, they
>>  are just the 16 bytes of state after hashing those single key blocks.
>>  A full MD5 hash gets finalized by padding out the data with nulls and
>>  a 64-bit count of the hashed data. If you  were to do a normal MD5 of
>>  Ki and Ko, the result would include the result of the finalization
>>  step.
>>
>>  You can see this in the hmac_md5_get_cram_context and
>>  hmac_md5_set_cram_context functions in the Dovecot code. The former
>>  extracts just the 4 32-bit state values, and the latter sets them and
>>  sets the bit count in the context structure to 512 (i.e. 64 bytes)
>>
>>  Maybe that provides a helpful clue...
>>
>
>Thank you very much for the time you spent looking into this.

Some people spend their weekends doing crosswords or Sudoku...

>There are still some wholes in the algorithm, but so far:
>
>1. Do the first part of HMAC as normal up to XOR'ing with 0x5c, including
>strangely enough what looks like a full (i.e. including nulling and
>padding) md5 hash if the passphrase is longer than 64 (this step should be
>feasible to replicate)

The HMAC specification is clear on that: if the shared secret (i.e. 
the password/passphrase) is longer than the hash function blocksize, 
it is hashed first and the hash output used as the key instead of the 
secret itself.

>2. Do the first step of md5 hashing (again, using the Perl library you
>suggested, it should be feasible to replicate the required functionality)
>
>-- From now on, it gets a bit sketchy
>
>3. Using the intermediate states of Ko and Ki, extract the various segments
>into a char[] so you end up with an array of 32 8bit segments. I'm unsure
>exactly what the bitwise AND operator is used for, I thought that a bitwise
>AND with 0xFF would leave everything the same?

Are you looking at the RFC1321 reference code's Encode function? The 
point there is that the input is an array of 32-bit unsigned integers 
(UINT4), the output is an array of (8-bit) characters. Doing an AND 
with 0xFF (an 8-bit value) on bit-shifted UINT4's is a way of 
clearing  everything but the 8 bits of interest and making the 
typecast formally correct. In a compilation and runtime environment 
that will let you play freely with pointer types, that function would 
be unneeded because you could just address the input as a char * 
instead of a UINT * and be done with it, but the formal approach 
Rivest uses assures that the bits are cleanly copied from an array of 
32-bit members to an array of 8-bit members even on systems where a 
pointer cast would not give correct results.

It is also important to keep the distinction between the HMAC and MD5 
layers clear. HMAC can be used with any blockwise hash function, and 
the internal details of the hash function are not important to the 
HMAC layer.

I'd break it up this way, with a warning that I'm hand-waving the 
mathematical innards of MD5:

1. If the password is longer than 64 bytes, run a full normal MD5 on 
it and use the 16-byte result as your shared secret. Otherwise, the 
password IS the shared secret. In practice, it would be a good idea 
to simply restrict passwords in size and content because 
authentication clients (i.e. MUA's) are going to have issues with 
reproducing passwords that have non-ASCII, whitespace, and control 
characters.

2. Generate the 2 MD5 "keys" from the shared secret. In Perl this 
might look like this:

$Ki = "$secret" ^ (chr(0x36) x 64);
$Ko = "$secret" ^ (chr(0x5c) x 64);

Keep in mind that the keys are special to HMAC, but as far as MD5 is 
concerned they are just the first 64-byte/512-bit blocks fed into the 
hash.

3. Initialize a fresh MD5 hash structure (if you are using a full MD5 
implementation ) or just a 'state' array containing the 4 32-bit 
values of the standard MD5 initialization vector (if you are doing 
your own implemetation or have direct access to a MD5Transform 
equivalent.)

4. Add Ki to the hash. In reference code terms, this would be a 
MD5Update call with Ki as the input. Note that most of the logic in 
the reference code MD5Update is about supporting the addition of data 
to the hash in chunks that don't fill 64-byte transformation blocks 
perfectly. If you are doing your own implementation just for this 
application, MD5Update is logically unnecessary because you can just 
pass your MD5Transform-equivalent a state array with the standard MD5 
initialization vector and Ki (which is a 64-character array)

5. The state array now holds the 'inner' context.

6. Repeat 3-4 with another fresh hash structure or state array and Ko.

7. The state array now holds the 'outer' context,


>4. Display a hex representation of the array contents.

Rendering those numbers as a hex string in the right order may not be 
obvious...

>I'll have a go later today and report back any findings. Of course, I could
>have completely misunderstood one of the steps, and that'll screw up the
>others, but still.

If OO Perl is your thing, I think that Digest::Perl::MD5 has 
everything you need except a public interface to the state array. You 
can still get to it however, and to the subroutine that does the hex 
string rendering...

In fact, look here:

http://www.scconsult.com/bill/crampass.pl

It does have a dependency issue, in that it requires Digest::Perl::MD5.


-- 
Bill Cole
bill at scconsult.com



More information about the dovecot mailing list