20120508

HMAC and cheese with Hash browns

Get it!? Because food?! Ehhhh, yeah, it was pretty weak.

I've been cranking on the next step of the TLS handshake--computing the master secret. Well, it's one thing to compute it, but another thing to tell if you did it right. This is a pretty big chunk of work, and I have serious reservations about being able to get it right without a lot of debugging... but that's not what I'm here to talk about.

No, today we're talking about HMAC, which is a way to hash some data, but also include some key material, so that only another party who also knows the key material can arrive at the same hash value. It can be used as part of a signature on some data, if you only want certain parties to be able to verify it.

I read the HMAC RFC, then implemented it. It wasn't hard, since I already had primitive hashing working. They give you the forumla for it pretty much like a cookbook; if you just follow the steps, it's hard to screw up.

But then I realized that this violates one of the principles of the MungeTLS project: don't re-implement cryptographic primitives. Arguably, HMAC is a cryptographic primitive, especially in relation to the TLS protocol. So I needed to get a Windows implementation of HMAC. It's there, but as usual, there was some wrangling involved.

My wrangles: let me show you them.

Like other hashes on Windows, you will be using CryptCreateHash and its buddies to make the hash, add data to it, and pull the hash value at the end. We start off in the usual way, by setting the ALG_ID parameter to CALG_HMAC.

It was during my prototyping of this code that I discovered that the more advanced cryptographic providers in Windows, such as the AES Cryptographic Provider are backwards comptible with the simpler ones, like the Base Cryptographic Provider. I just used the AES provider, since I wasn't sure at compile-time what algorithm I'd be using.

Speaking of algorithms, supplying CALG_HMAC isn't enough, if you read the RFC. HMAC is based on another hash function, so we also have to choose this "inner" hash algorithm. The way we indicate this is by passing a HMAC_INFO structure to CryptSetHashParam with HP_HMAC_INFO.

HMAC_INFO hinfo = {0};
hinfo.HashAlgid = CALG_SHA_256;

if (!CryptSetHashParam(
         hHash,
         HP_HMAC_INFO,
         reinterpret_cast<const BYTE*>(&hinfo),
         NULL))
{
    wprintf(L"CryptSetHashParam\n");
    hr = HRESULT_FROM_WIN32(GetLastError());
    goto error;
}

The second important part of the HMAC is, of course, the H--that is, the key material. Actually, CryptCreateHash has a parameter for this: our old friend HCRYPTKEY. We need a symmetric key to pass in here. You can use any of the usual ways to get this key, typically CryptGenKey or CryptDeriveKey.

But in TLS, we aren't generating the key ourselves; the key is an arbitary series of bytes that originate from the other party over the network. We'll have to import the secret as a key before we can use it here.

In a previous post, I attached the code for exporting a symmetric key. Thankfully, the process of importing one is just the reverse. I was scared off a bit by the documentation for CryptImportKey, which says stuff like "For all but the public key, the key or key pair is encrypted." That would mean I'd have to jump through a bunch of annoying, weird hoops just to import the thing. Thankfully, the documentation is just self-contradictory on this point: it goes on later to say that our friend the PLAINTEXTKEYBLOB also doesn't need to be encrypted.

// from http://msdn.microsoft.com/en-us/library/windows/desktop/aa379931(v=vs.85).aspx
// see note about PLAINTEXTKEYBLOB
struct PlaintextKey
{
    BLOBHEADER hdr;
    DWORD cbKeySize;
    BYTE rgbKeyData[1];
};

vector<BYTE> vbKey; // contains raw bytes of the key
vector<BYTE> vbPlaintextKey;
vbPlaintextKey.resize(sizeof(PlaintextKey) + vbKey.size());

PlaintextKey* pPlaintextKey = reinterpret_cast<PlaintextKey*>(&vbPlaintextKey.front());
pPlaintextKey->hdr.bType = PLAINTEXTKEYBLOB;
pPlaintextKey->hdr.bVersion = CUR_BLOB_VERSION;
pPlaintextKey->hdr.aiKeyAlg = CALG_RC2;
pPlaintextKey->cbKeySize = vbKey.size();

copy(vbKey.begin(), vbKey.end(), pPlaintextKey->rgbKeyData);

if (!CryptImportKey(
         hProv,
         reinterpret_cast<const BYTE*>(pPlaintextKey),
         vbPlaintextKey.size(),
         NULL,
         CRYPT_IPSEC_HMAC_KEY,
         &hKey))
{
    hr = HRESULT_FROM_WIN32(GetLastError());
    wprintf(L"failed CryptImportKey: %08LX\n", hr);
    goto error;
}

Two odd things to note here are that I've fixed the key algorithm as RC2, and that I'm passing this weird flag, CRYPT_IPSEC_HMAC_KEY. The docs for CryptImportKey say that for HMAC keys, which don't really have their own algorithm--they're just generic "secrets", RC2 is specified... just because. Whatever, I'll do that. If your HMAC key is larger than 16 bytes (in TLS they most assuredly are), you have to pass this arbitrary IPSec related flag. Meh, fine. Caused me about 15 minutes' worth of digging around to discover that.

Once we have the key, calculating the HMAC is as simple as plugging it into the CryptCreateHash call above. I'm not going to bother putting up the code this time, since it's pretty straightforward from the snippets I already included.

0 comments:

Post a Comment