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 :)