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..]; }} 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)}"); // Usually no conversion is needed: ECDsa reads and writes both encodingsbool viaDer = key.VerifyData(message, der, HashAlgorithmName.SHA256, DSASignatureFormat.Rfc3279DerSequence);bool viaRaw = key.VerifyData(message, raw, HashAlgorithmName.SHA256, DSASignatureFormat.IeeeP1363FixedFieldConcatenation); How it works
AsnReaderwithAsnEncodingRules.DERreads the SEQUENCE and both INTEGERs;ThrowIfNotEmptyrejects trailing bytes after the signature and inside it.ReadIntegerBytesreturns the two's complement bytes: a leading0x00when the top bit is set, fewer than 32 bytes when the value is small.WriteFixedstrips the sign byte and right-aligns the value in its half of the output.WriteIntegerUnsignedproduces 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 whyRawToDertrims them first.AsnWriterpicks the long-form length automatically, so 132-byte P-521 signatures need no special case; the run round-trips 1,000 of them.
Gotchas
WriteIntegerUnsignedthrowsArgumentException: The first 9 bits of the integer value all have the same valuewhen a raw half starts with0x00— 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 missingDSASignatureFormatargument rather than a need to convert. sizemust 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.AsnReaderin DER mode rejects BER-style encodings (non-minimal lengths, padded integers) withAsnContentException. A signature that some other library accepted may be sloppily encoded — inspect it in the converter rather than relaxing the rules.