Skip to content
ecdsa.com

Convert DER ⇄ raw · .NET

How to convert an ECDSA signature between DER and raw in C# (.NET)

To convert an ECDSA signature between DER and raw in C# — ASN.1 DER on one side, r‖s on the other — use AsnReader and AsnWriter from System.Formats.Asn1; they ship with .NET and enforce the DER rules for you. Often the better answer is not to convert at all: .NET's ECDsa signs and verifies both encodings through the DSASignatureFormat parameter.

Tested with .NET 10.0.13 (SDK 10.0.402) (Linux, System.Formats.Asn1, OpenSSL 3.0.13 backend).

SignatureFormats.cs — DER ⇄ r‖s with System.Formats.Asn1
using System.Formats.Asn1;
static class SignatureFormats
{
/// <summary>DER SEQUENCE { INTEGER r, INTEGER s } to r‖s; size: 32 P-256, 48 P-384, 66 P-521.</summary>
public static byte[] DerToRaw(byte[] der, int size)
{
var reader = new AsnReader(der, AsnEncodingRules.DER); // strict: rejects non-minimal encodings
AsnReader sequence = reader.ReadSequence();
reader.ThrowIfNotEmpty();
byte[] raw = new byte[2 * size];
WriteFixed(sequence.ReadIntegerBytes().Span, raw.AsSpan(0, size));
WriteFixed(sequence.ReadIntegerBytes().Span, raw.AsSpan(size));
sequence.ThrowIfNotEmpty();
return raw;
}
/// <summary>r‖s to DER; WriteIntegerUnsigned adds the 0x00 sign byte where needed.</summary>
public static byte[] RawToDer(byte[] raw)
{
int half = raw.Length / 2;
var writer = new AsnWriter(AsnEncodingRules.DER);
using (writer.PushSequence())
{
writer.WriteIntegerUnsigned(TrimLeadingZeros(raw.AsSpan(0, half)));
writer.WriteIntegerUnsigned(TrimLeadingZeros(raw.AsSpan(half)));
}
return writer.Encode();
}
static void WriteFixed(ReadOnlySpan<byte> integer, Span<byte> target)
{
if (integer.Length > 1 && integer[0] == 0) integer = integer[1..]; // the sign byte
if (integer.Length > target.Length) throw new FormatException("integer does not fit the curve");
integer.CopyTo(target[(target.Length - integer.Length)..]); // left-pad with zeros
}
static ReadOnlySpan<byte> TrimLeadingZeros(ReadOnlySpan<byte> value)
{
int i = 0;
while (i < value.Length - 1 && value[i] == 0) i++;
return value[i..];
}
}
convert the shared example signature and check the round trip
byte[] der = Convert.FromHexString("30450221009f4118b059a34a2f344c39a1d085d93bab560402938281dc1cb2f3dfb8eee58602200bd54b34a72042551c3316e10f461a1d69036ba4f0d72d95d42465340b56def8");
byte[] raw = SignatureFormats.DerToRaw(der, 32);
Console.WriteLine($"raw: {Convert.ToHexStringLower(raw)}");
Console.WriteLine($"round trip: {SignatureFormats.RawToDer(raw).AsSpan().SequenceEqual(der)}");
skip the conversion: pass the format to VerifyData
// Usually no conversion is needed: ECDsa reads and writes both encodings
bool viaDer = key.VerifyData(message, der, HashAlgorithmName.SHA256, DSASignatureFormat.Rfc3279DerSequence);
bool viaRaw = key.VerifyData(message, raw, HashAlgorithmName.SHA256, DSASignatureFormat.IeeeP1363FixedFieldConcatenation);

How it works

  1. AsnReader with AsnEncodingRules.DER reads the SEQUENCE and both INTEGERs; ThrowIfNotEmpty rejects trailing bytes after the signature and inside it.
  2. ReadIntegerBytes returns the two's complement bytes: a leading 0x00 when the top bit is set, fewer than 32 bytes when the value is small. WriteFixed strips the sign byte and right-aligns the value in its half of the output.
  3. WriteIntegerUnsigned produces a minimal DER INTEGER from an unsigned big-endian value and adds the sign byte itself — but it requires the input without redundant leading zeros, which is why RawToDer trims them first.
  4. AsnWriter picks the long-form length automatically, so 132-byte P-521 signatures need no special case; the run round-trips 1,000 of them.

Gotchas

  • WriteIntegerUnsigned throws ArgumentException: The first 9 bits of the integer value all have the same value when a raw half starts with 0x00 — as it does for the example signatures with a 31-byte r. Trim leading zeros before writing.
  • In .NET the default signature format is raw (IeeeP1363FixedFieldConcatenation), the opposite of OpenSSL and Java. Many "conversion" bugs are a missing DSASignatureFormat argument rather than a need to convert.
  • size must match the curve (32, 48 or 66 bytes). A P-384 signature converted with 32 throws "integer does not fit the curve" instead of being truncated.
  • AsnReader in DER mode rejects BER-style encodings (non-minimal lengths, padded integers) with AsnContentException. A signature that some other library accepted may be sloppily encoded — inspect it in the converter rather than relaxing the rules.

Related recipes