Skip to content
ecdsa.com

Sign a message · .NET

How to sign a message with ECDSA in C# (.NET)

To sign a message with ECDSA in C#, load the private key with ECDsa.ImportFromPem and call SignData — System.Security.Cryptography does the hashing and signing, no package required. One parameter decides who can verify the result: .NET writes raw r‖s by default, and DSASignatureFormat.Rfc3279DerSequence switches to the DER that OpenSSL, Java and Go expect.

Tested with .NET 10.0.13 (SDK 10.0.402) (Linux, OpenSSL 3.0.13 backend) and OpenSSL 3.0.13 (openssl dgst cross-check).

sign.cs — DER and raw r‖s signatures from the same key
using System.Security.Cryptography;
using var privateKey = ECDsa.Create();
privateKey.ImportFromPem(File.ReadAllText("key.pem")); // "PRIVATE KEY" or "EC PRIVATE KEY"
byte[] message = File.ReadAllBytes("message.txt");
// DER for OpenSSL, Java, Go and Python verifiers
byte[] der = privateKey.SignData(message, HashAlgorithmName.SHA256, DSASignatureFormat.Rfc3279DerSequence);
File.WriteAllBytes("sig.der", der);
Console.WriteLine($"DER signature: {der.Length} bytes");
// .NET's default: raw r‖s, the JWS / WebCrypto layout
byte[] raw = privateKey.SignData(message, HashAlgorithmName.SHA256);
File.WriteAllBytes("sig.raw", raw);
Console.WriteLine($"raw signature: {raw.Length} bytes");
sign a large file: hash the stream, then SignHash
// Large files: hash the stream once, then sign the 32-byte digest
using (FileStream file = File.OpenRead("message.txt"))
{
byte[] digest = SHA256.HashData(file);
byte[] signature = privateKey.SignHash(digest, DSASignatureFormat.Rfc3279DerSequence);
File.WriteAllBytes("sig-stream.der", signature);
}

How it works

  1. ImportFromPem accepts both BEGIN PRIVATE KEY (PKCS#8) and the legacy BEGIN EC PRIVATE KEY (SEC 1) without conversion.
  2. SignData(message, HashAlgorithmName.SHA256, …) hashes the message and signs the digest; the run verifies the DER result with openssl dgst -verify over the same file.
  3. Without a format argument the output is IeeeP1363FixedFieldConcatenation — always 64 bytes on P-256, ready for a JWS. DER output varies between 70 and 72 bytes.
  4. SHA256.HashData(Stream) reads a file in chunks, and SignHash signs that digest. The signature is the same kind SignData produces over the whole message, so OpenSSL verifies it against the original file.

Gotchas

  • The default output format is the opposite of Java's and OpenSSL's. A raw signature sent to a DER verifier fails there with a parse error or a plain "verification failure" — choose the format explicitly on both sides.
  • Signatures differ on every call: .NET uses a random nonce (the OpenSSL or CNG implementation underneath), and does not offer RFC 6979 deterministic signing. Compare signatures by verifying, never byte for byte.
  • Passing a digest to SignData hashes it a second time. Digests go to SignHash, messages to SignData.
  • An encrypted private key PEM throws An encrypted key was found, but no password was provided — use ImportFromEncryptedPem(pem, password).

Related recipes