2
votes

I generated a public key using OpenSSL with the following commands to create a 1024bit RSA key, and then export the public key, giving me the public key in a .pem file.

> openssl genrsa -out private_key.pem 1024
> openssl rsa -pubout -in private_key.pem -out public_key.pem

I'm using the private key to sign some data in PHP, and want to verify the signature in Windows using the CryptoAPI. I would prefer not to link to the Windows OpenSSL port if possible.

The public key is in text format. How do I successfully load this and use it to verify a signature?

Current approach

I'm trying to use CryptImportKey to import a public key blob with the format:

  • PUBLICKEYSTRUC
  • Size of the following array of bytes, DWORD
  • Key as array of bytes (narrow / ANSI string)

in a packed structure.

The key byte array is simply the contents of the public key .pem file, including the BEGIN and END statements, as an ANSI (non-Unicode) string. Note that I am using a Unicode app, but the Crypto APIs don't seem to come in ANSI/Unicode versions. (Do they?)

When I do this, CryptImportKey fails with GetLastError having a value of 0x80090005, NTE_BAD_DATA. I'm not sure why this is... thus the question :)

Code

The public key blob structure is defined as:

TPublicKeyBlob = packed record
  strict private
    FHeader : BLOBHEADER;
    FKeyLength : DWORD;
    FKey : array[0..4095] of Byte;
  public
    procedure Init(const KeyStr : string);
    function Size : DWORD;
  end;

and the methods there are:

procedure TPublicKeyBlob.Init(const KeyStr: string);
var
  KeyAnsi : AnsiString;
begin
  KeyAnsi := AnsiString(KeyStr);
  if Length(KeyAnsi) > Length(FKey) then
    raise Exception.Create('Key too long');

  FHeader.bType := PLAINTEXTKEYBLOB;
  FHeader.bVersion := CUR_BLOB_VERSION;
  FHeader.reserved := 0;
  FHeader.aiKeyAlg := CALG_RSA_KEYX;

  FKeyLength := Length(KeyAnsi) * sizeof(KeyAnsi[1]);
  assert(sizeof(KeyAnsi[1]) = 1); // Should be single byte strings!

  Move(KeyAnsi[1], FKey[0], Length(KeyAnsi) * sizeof(KeyAnsi[1]));
end;

function TPublicKeyBlob.Size: DWORD;
begin
  // Return only the used size - not all the byte array will be used
  Result := SizeOf(FHeader) + Sizeof(FKeyLength) + FKeyLength;
end;

This should create an in-memory layout matching the example in Microsoft's Importing a Plaintext Key example. Maybe. Is CALG_RSA_KEYX correct for a RSA 1024-bit key (or rather, the generated public key from one)? Is my usage of Move() correct? (Should be the analog of memcpy, if you are a C programmer, but params are source, dest, size, and strings are 1-indexed but the array is 0-indexed.) Should Size() return the size of the whole structure, or just the key array?

The parameter is the public key (below) as a string.

This is then used as follows:

function TLicenseInfo.VerifySignature: Boolean;
const
  PublicKeyStr = '-----BEGIN PUBLIC KEY-----' +
    'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCeyh3x8qbGgq+ZlXm4erhjFMKY' +
    'RGhAyFc8DUupkaLlSSzXDcHHC27VtuxAKtAVvm97OGJMbdbtAUy6rmVH0GSQmP+0' +
    'Mct+7ncQRfpXJ4kAYg3gpv4dSsBl83rWDuQ06QDPjIT7eMdNMuUTm11GIYFnvyv4' +
    'C9Vn92hBC2QeRRlslwIDAQAB' +
    '-----END PUBLIC KEY-----';
var
  hCryptProv, hHash: wcrypt2.HCRYPTPROV;
  hKey: Cardinal;
  PublicKeyBlob : TPublicKeyBlob;
begin
  Result := False;

  PublicKeyBlob.Init(PublicKeyStr);

  if not CryptAcquireContext(@hCryptProv, 'Test', nil, PROV_RSA_FULL, 0) then
    if not CryptAcquireContext(@hCryptProv, 'Test', nil, PROV_RSA_FULL, CRYPT_NEWKEYSET) then
      Exit;

  try
    if CryptImportKey(hCryptProv, @PublicKeyBlob, PublicKeyBlob.Size, 0, 0, @hKey) then
    // ^ this is the line that fails
    begin
      // ... check the signature.
    end;
  finally
    CryptReleaseContext(hCryptProv, 0);
  end;
end;

The marked line calling CryptImportKey fails and GetLastError returns 0x80090005, NTE_BAD_DATA.

I've tried quite a few variations - some variants of the algorithm, experimenting with the size, removing the BEGIN and END portions of the key string, etc. But I'm not familiar with the Crypto API and I'm sure it's something obvious to others :)

1
Try Base64-decoding the key first (plaintext in this context I think means it's not encrypted, not that it's actually in ASCII format)Jonathan Potter
@JonathanPotter Ah... I think that is one of those "oh, of course..." moments. Gah. I'll try it and update :)David
Decoding results in the same error code in the same place. Note I had to strip the BEGIN/END parts of the key in order to decode it, and I'm not sure that's correct - I think a plaintext key requires those as part of its format.David

1 Answers

1
votes

I have answered a similar question many years ago (on 2012): https://stackoverflow.com/a/10827239/707093

Basically, you have to use CryptStringToBinary to get decode the BASE64 value and then use CryptDecodeObjectEx and CryptImportKey. The C sample that shows how to do this is at https://www.idrix.fr/Root/Samples/capi_pem.cpp. Translating this to delphi is not too difficult.

I hope this will help.