5

I need to access Amazon REST services like a previous question at " HMAC-SHA256 in Delphi " asked. Since this has to be in D2010 I'm trying to use the latest libeay32.dll to pass the test vectors in RFC 4231:

https://www.rfc-editor.org/rfc/rfc4231

Does anyone have a method that passes these tests in Delphi using this library? The code posted by shunty in the post I referred to passes the first two test vectors as well as the fifth, but it fails in the third and fourth. Those vectors are over 64 bytes and since all of the url's that I need to sign for Amazon are over 64 bytes this is a problem. I haven't been able to figure out if I'm doing something wrong. The OpenSSL test is in hmactest.c, but it only checks EVP_md5 and the test vectors aren't all the same as in the RFC. I need this to work with SHA256 so I can verify against the test vectors in the RFC. I'm using the following constants for the tests (constants now updated for future viewers to fix my copy and paste errors mentioned in the comments below):

const
  LIBEAY_DLL_NAME = 'libeay32.dll';
  EVP_MAX_MD_SIZE = 64;

  //RFC 4231 Test case 1
  TEST1_KEY: string = '0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b';
  TEST1_DATA: string = '4869205468657265';
  TEST1_DIGEST: string = 'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7';

  //RFC 4231 Test case 2
  TEST2_KEY = '4a656665';
  TEST2_DATA = '7768617420646f2079612077616e7420666f72206e6f7468696e673f';
  TEST2_DIGEST = '5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843';

  //RFC 4231 Test case 3
  TEST3_KEY = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
  TEST3_DATA = 'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd';
  TEST3_DIGEST = '773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe';

  //RFC 4231 Test case 4
  TEST4_KEY = '0102030405060708090a0b0c0d0e0f10111213141516171819';
  TEST4_DATA = 'cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd';
  TEST4_DIGEST = '82558a389a443c0ea4cc819899f2083a85f0faa3e578f8077a2e3ff46729665b';

  //RFC 4231 Test case 5
  TEST5_KEY = '0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c';
  TEST5_DATA = '546573742057697468205472756e636174696f6e';
  TEST5_DIGEST = 'a3b6167473100ee06e0c796c2955552b';

I don't know how this code by shunty will look pasted in because it looks terrible here (I'm a stackoverflow novice). I used RAND_seed rather than RAND_load_file like he did, but otherwise it's the same:

function TForm1.GetHMAC(const AKey, AData: string): TBytes;
var
  key, data: TBytes;
  md_len: integer;
  res: PByte;
  buf: PInteger;
  rand_val: Integer;
begin
  OpenSSL_add_all_algorithms;

  Randomize;
  rand_val := Random(100);
  GetMem(buf, rand_val);
  try
    RAND_seed(buf, rand_val);

    key := TEncoding.UTF8.GetBytes(AKey);
    data := TEncoding.UTF8.GetBytes(AData);
    md_len := EVP_MAX_MD_SIZE;
    SetLength(Result, md_len);
    res := HMAC(EVP_sha256, @key[0], Length(key), @data[0], Length(data), @result[0], md_len);
    if (res <> nil) then
      SetLength(Result, md_len);

  finally
    FreeMem(buf);
  end;
end;

The code I use to test looks like this. This particular method is for test 3 which fails. The result is bb861233f283aef2ef7aea09785245c9f3c62720c9d04e0c232789f27a586e44, but it should be equal to the constant hex value for TEST3_DIGEST:

procedure TForm1.btnTestCase3Click(Sender: TObject);
var
  LBytesDigest: TBytes;
  LHashString: string;
  LHexDigest: string;
begin
  LBytesDigest := GetHMAC(HexToStr(TEST3_KEY), HexToStr(TEST3_DATA));

  LHexDigest := LowerCase(BytesToHex(LBytesDigest));

  if LHexDigest = TEST3_DIGEST then begin
    Memo1.Lines.Add('SUCCESS: Matches test case');
    Memo1.Lines.Add(LHexDigest);
  end else begin
    Memo1.Lines.Add('ERROR: Does not match test case');
    Memo1.Lines.Add('Result:    ' + LHexDigest);
    Memo1.Lines.Add('Test Case: ' + TEST3_DIGEST);
  end;
end;

Any ideas? I'm about to give up and just create a .NET app using the library they provide...

Community
  • 1
  • 1
Ron Grove
  • 85
  • 6

1 Answers1

6

You are using D2009+ (as evident by your use of TEncoding), which mean you are dealing with UnicodeString, but you are not taking Unicode into account in your logic. The RFC does not operate on characters, it operates on bytes. Your test data contains hex-encoded strings. When you decode them into (Unicode)String values, many of the resulting characters are outside of the ASCII range of characters, which means they have to be interpretted by Ansi codepages before you can convert them to UTF-8 correctly (which you should not be using in this situation anyway).

You need to change your implementation to decode your hex strings straight to TBytes instead (you can use Classes.HexToBin() for that) so the correct byte values are preserved and passed to HMAC, and get rid of TEncoding.UTF8.GetBytes() completely:

function TForm1.GetHMAC(const AKey, AData: TBytes): TBytes; 
var 
  md_len: integer; 
  res: PByte; 
  buf: PInteger; 
  rand_val: Integer; 
begin 
  OpenSSL_add_all_algorithms; 

  Randomize; 
  rand_val := Random(100); 
  GetMem(buf, rand_val); 
  try 
    RAND_seed(buf, rand_val); 
    md_len := EVP_MAX_MD_SIZE; 
    SetLength(Result, md_len); 
    res := HMAC(EVP_sha256, Pointer(AKey), Length(AKey), Pointer(AData), Length(AData), @Result[0], md_len); 
    if (res <> nil) then 
      SetLength(Result, md_len); 
  finally 
    FreeMem(buf); 
  end; 
end; 

function HexToBytes(const S: String): TBytes;
begin
  SetLength(Result, Length(S) div 2);
  SetLength(Result, HexToBin(PChar(S), Pointer(Result), Length(Result)));
en;

procedure TForm1.btnTestCase3Click(Sender: TObject);  
var  
  LBytesDigest: TBytes;  
  LHashString: string;  
  LHexDigest: string;  
begin  
  LBytesDigest := GetHMAC(HexToBytes(TEST3_KEY), HexToBytes(TEST3_DATA));  
  LHexDigest := LowerCase(BytesToHex(LBytesDigest));  
  if LHexDigest = TEST3_DIGEST then begin  
    Memo1.Lines.Add('SUCCESS: Matches test case');  
    Memo1.Lines.Add(LHexDigest);  
  end else begin  
    Memo1.Lines.Add('ERROR: Does not match test case');  
    Memo1.Lines.Add('Result:    ' + LHexDigest);  
    Memo1.Lines.Add('Test Case: ' + TEST3_DIGEST);  
  end;  
end;  
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • In my defence, the UTF8.GetBytes was used because that was what the internals of the particular app we were working on required - we were never interacting with anything outside the app. And there is a disclaimer at the bottom of the post stating that I hadn't used it against reference data. And we have improved over the last two years :-) – shunty Apr 03 '12 at 06:50
  • Interesting. At one point I had an overloaded GetHMAC that took TBytes as the parameters. Not sure if I was using the Hex encoded tests from the RFC at the time, though. It probably didn't work because of some Unicode issues. I've never had to deal with it directly like this before. For Amazon you have to take a URI, convert it with HMAC SHA1 or SHA256, then convert that to Base64, then URL encode that, then add the signature to the original URI. In Delphi it's like walking from one shark tank into another and into another again. The code Amazon's .NET library uses is so simple by comparison. – Ron Grove Apr 03 '12 at 07:17
  • @shunty - Thanks for the original post. Looks like my comment that you did say you hadn't tested it against any reference data got dropped at some point in editing. That was fine because your disclaimer is what made me find the RFC and try to get their hex encoded tests to work. – Ron Grove Apr 03 '12 at 07:28
  • Still doesn't match the third RFC test vector. I'm getting 773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe now. – Ron Grove Apr 03 '12 at 08:09
  • @Ron - I'm guessing test 3 is failing because you're testing against the hmac for 384 bits not 256. Bit of copy & paste error from the RFC I reckon :-) – shunty Apr 03 '12 at 12:25
  • Ah, thanks guys! Happens when you change things around too much. I see in my history that they got messed up in the last hour of the workday as I was trying different things. They all match now. I'll turn this into a DUnit test and put it on my website for others in the future. – Ron Grove Apr 03 '12 at 15:48