There are different signature formats, one is the ASN.1 DER format, which is used in your code and is explained here. For curve secp128r1 (P-128) the generated signatures have a length between 38 and 40 bytes.
The format you seem to expect is r | s (see here), which for curve secp128r1 has a length of 32 bytes.
You can easily convert between the two formats manually.
Newer versions of BouncyCastle for C# also allow the direct generation of the signature in the r | s format, see here, e.g. with SHA-256withPLAIN-ECDSA
(instead of SHA-256withECDSA
).
A few notes:
- According to NIST, a key size of 128 bits is meanwhile too small (instead, at least 224 is recommended for 2019-2030), here. secp128r1 was still recommended in SEC 2: Recommended Elliptic Curve Domain Parameters V1.0 (from 2000), but no longer in SEC 2: Recommended Elliptic Curve Domain Parameters V2.0 (from 2010).
SHA-256withECDSA
(SHA-256withPLAIN-ECDSA
) hashes the message implicitly (with SHA-256), i.e. the explicit hashing is not necessary and results in a double (redundant) hashing.
- Furthermore, in your explicit hashing, not the actual binary hash is passed to the signer, but the UTF-8 encoded hexadecimal encoded hash.
- secp128r1 corresponds to a key size (order of the base point) of 128 bits (16 bytes). But with SHA256, you are using a digest that is twice as large (32 bytes). In this case according to NIST FIPS 186-4 the leftmost n bits of the hash are used, here.
Edit:
To concretise what has been said so far: The following code UTF-8 encodes the string message
and generates an ECDSA signature in r|s format (which corresponds to IEEE P1363, here) based on curve secp128r1 and digest SHA256:
public static string GenerateSignature(string message, string privateKeyPath)
{
byte[] messageBytes = Encoding.UTF8.GetBytes(message);
ISigner signer = SignerUtilities.GetSigner("SHA-256withPLAIN-ECDSA");
AsymmetricCipherKeyPair keyPair = getPrivateKeyFromPemFile(privateKeyPath); // get secp128r1 key pair
signer.Init(true, keyPair.Private);
signer.BlockUpdate(messageBytes, 0, messageBytes.Length);
byte[] signature = signer.GenerateSignature();
return ByteArrayToString(signature); // https://stackoverflow.com/a/311179/9014097
}
Note the differences compared to the code you originally posted:
- Since (
Org.BouncyCastle.Crypto.
) ISigner#GenerateSignature()
hashes implicitly, no explicit hashing is performed.
SHA-256withPLAIN-ECDSA
generates the signature directly in r|s format and exists since v1.8.4 (bccrypto-csharp-1.8.4, released 27th October 2018).
In case of an older BouncyCastle version (where only SHA-256withECDSA
is available), the signature has to be converted manually from ASN.1 DER to r|s format. This is easy, since r and s can be extracted directly from the ASN.1 DER format, see here.
If the verification on the client side still fails with the above code, check that the keys, curves and digests on both sides match and the verification process itself.
For completeness:
Of course, also (Org.BouncyCastle.Crypto.Signers.
) ECDsaSigner.GenerateSignature(byte[] msg)
can be used to generate the signature. It is important that this method does not hash implicitly, i.e. here indeed you must hash explicitly. This method returns r and s as (Org.BouncyCastle.Math.
) BigInteger[]
so that both parts can be easily hex encoded with e.g. toString(16)
and then concatenated to r|s.