For a project I'm working on, I need to verify signatures of data records. We have a legacy project that makes use of an imported DLL of which the source code is no longer available. We are now updating the project, and would like to also modernize the Signature verification.
The supported signature-methods are RSA 1024, ECDSA 192, ECDSA 224 and ECDSA 256.
As example data I have the following public key: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwhDZNphi4J1EiBCQwZ1WBCYbnf9Ne6UqPuLLGJPTj5++io9pxT3McYgFFFPb7HdfJCrPsKmTVwlw9Z1RpQru6g==
, the following signature:
BEgwRgIhAPqcBPjRROiwTBUFTF50iDXSBn+e71c3fIj9kgfxUbAZAiEAR6KM+u8D6TWtaKVdH8EjLxWJf4Vm/u2Z5YptKEjTgHs=
, and the following data that should be verified to: ;"4399901879126";"302.1302.006";"4005249001296";"20240627151654328";
I have verified with the legacy tool that these values should give a positive result.
I've tried some of the following, but no dice:
private static void Main(string[] args)
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
var publicKey = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwhDZNphi4J1EiBCQwZ1WBCYbnf9Ne6UqPuLLGJPTj5++io9pxT3McYgFFFPb7HdfJCrPsKmTVwlw9Z1RpQru6g==";
var signature = "BEgwRgIhAPqcBPjRROiwTBUFTF50iDXSBn+e71c3fIj9kgfxUbAZAiEAR6KM+u8D6TWtaKVdH8EjLxWJf4Vm/u2Z5YptKEjTgHs=";
var data = ";\"4399901879126\";\"302.1302.006\";\"4005249001296\";\"20240627151654328\";";
var value = VerifyEcdsaSignature(Convert.FromBase64String(publicKey), Encoding.Latin1.GetBytes(data), Convert.FromBase64String(signature));
Console.WriteLine(value);
Console.ReadLine();
}
public static bool VerifySignature(byte[] key, byte[] data, byte[] signature)
{
var publicKey = PublicKey.CreateFromSubjectPublicKeyInfo(key, out _);
var ecdsa = publicKey.GetECDsaPublicKey() ?? ECDsa.Create();
return ecdsa.VerifyData(data, signature, HashAlgorithmName.SHA256);
}
public static byte[] GetSha224Bytes(byte[] data)
{
return Sha224.Create().ComputeHash(data);
}
public static byte[] GetSha256Bytes(byte[] data)
{
return SHA256.HashData(data);
}
private static bool VerifyEcdsaSignature(byte[] key, byte[] bytesData, byte[] bytesSignature)
{
var publicKey = PublicKey.CreateFromSubjectPublicKeyInfo(key, out _);
var ecdsa = publicKey.GetECDsaPublicKey();
var pubKey = DotNetUtilities.GetECDsaPublicKey(ecdsa);
ISigner signer = SignerUtilities.GetSigner("SHA-256withECDSA");
signer.Init(false, pubKey);
signer.BlockUpdate(bytesData);
return signer.VerifySignature(bytesSignature);
}
The reason for the failed verification is an incompatible signature format and (probably) a bug in your legacy project!
This can be most easily explained using the hex encoded signature:
This signature is ASN.1/DER encoded (which can be checked with an ASN.1 parser such as https://lapo.it/asn1js/).
Most libraries support ASN.1/DER encoded signatures, but without the ID/length associated with OCTET STRING
, i.e. the two leading bytes 0x0448. If these are removed, the result is:
This signature should now actually be successfully verified if the correct signature format is specified.
As C# uses the IEEE P1363 format by default, the ASN.1/DER format must be explicitly specified with DSASignatureFormat.Rfc3279DerSequence
as 3rd parameter in the VerifyData()
call or (as already used) SHA-256withECDSA
in the BC variant.
Nonetheless, even this verification fails! The reason for this is that the legacy project does not seem to implement the ASN.1/DER format as intended (probably this is a bug).
An explanation of the correct format can be found here:
...the encodings of r and s are minimal-sized signed big-endian. Since r and s are positive values, this means that the top bit of the first byte of each encoding must be zero...
In other words, a leading 0x00 may only appear before r if the leading byte is greater than 0x7F. Likewise for s.
For the posted signature, the leading byte for r (0xFA9C...B019
) is greater than 0x7F, but not for s (0x47A2...807B
). Nevertheless, the 0x00 byte is also set for s. This is incorrect and must be fixed: The 0x00 byte must be removed and the length information at index positions 1 (0x45 instead of 0x46) and 38 (0x20 instead of 0x21) must be adjusted:
With this signature, verification is successful:
...
var publicKey = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwhDZNphi4J1EiBCQwZ1WBCYbnf9Ne6UqPuLLGJPTj5++io9pxT3McYgFFFPb7HdfJCrPsKmTVwlw9Z1RpQru6g==";
var signature = "MEUCIQD6nAT40UTosEwVBUxedIg10gZ/nu9XN3yI/ZIH8VGwGQIgR6KM+u8D6TWtaKVdH8EjLxWJf4Vm/u2Z5YptKEjTgHs=";
var data = ";\"4399901879126\";\"302.1302.006\";\"4005249001296\";\"20240627151654328\";";
var value = VerifySignature(Convert.FromBase64String(publicKey), Encoding.UTF8.GetBytes(data), Convert.FromBase64String(signature));
Console.WriteLine(value); // true
...
public static bool VerifySignature(byte[] key, byte[] data, byte[] signature)
{
...
return ecdsa.VerifyData(data, signature, HashAlgorithmName.SHA256, DSASignatureFormat.Rfc3279DerSequence); // apply ASN.1/DER format
}
...
You will probably have no choice but to check the signatures before verification and correct them if necessary (at least if the intended libraries are to be used).
As an alternative to the ASN.1/DER encoding, the signature can be converted to the IEEE P1363 format. In this format, r for P-256 has a fixed size of 32 bytes (and is padded to this size with leading 0x00 values if required). The same applies to s. The posted signature is in IEEE P1363 format:
0xFA9C04F8D144E8B04C15054C5E748835D2067F9EEF57377C88FD9207F151B01947A28CFAEF03E935AD68A55D1FC1232F15897F8566FEED99E58A6D2848D3807B
In the context of the native C# methods, this format must be specified with DSASignatureFormat.IeeeP1363FixedFieldConcatenation
(which is also the default).
In the context of BC, "SHA-256withPLAIN-ECDSA"
must be applied as algorithm