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 verifiersbyte[] 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 layoutbyte[] raw = privateKey.SignData(message, HashAlgorithmName.SHA256);File.WriteAllBytes("sig.raw", raw);Console.WriteLine($"raw signature: {raw.Length} bytes"); // Large files: hash the stream once, then sign the 32-byte digestusing (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
ImportFromPemaccepts bothBEGIN PRIVATE KEY(PKCS#8) and the legacyBEGIN EC PRIVATE KEY(SEC 1) without conversion.SignData(message, HashAlgorithmName.SHA256, …)hashes the message and signs the digest; the run verifies the DER result withopenssl dgst -verifyover the same file.- 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. SHA256.HashData(Stream)reads a file in chunks, andSignHashsigns that digest. The signature is the same kindSignDataproduces 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
SignDatahashes it a second time. Digests go toSignHash, messages toSignData. - An encrypted private key PEM throws
An encrypted key was found, but no password was provided— useImportFromEncryptedPem(pem, password).