ECDSA Signature: RAW - DER Conversion

by shahiN Noursalehi

You are here: Home / Tutorials / Cryptography, ECDSA, RAW - DER Conversion

Understanding ECDSA Signature Formats

In blockchain transactions, ECDSA signatures are encoded in RAW or DER formats. This tutorial explores converting between these formats, essential for interoperability between platforms like C# (RAW) and Java (DER). We cover the mathematical conversion, differences between compact and normal DER, the history and necessity of DER, pseudo-code, C# implementations, sample outputs, and the role of Base64 encoding.

Key Concepts

  • RAW Format: Concatenation of R and S components, 64 bytes for secp256r1 (32 bytes each).
  • DER Format: ASN.1-encoded SEQUENCE of R and S INTEGERs, typically 70-72 bytes.
  • Compact DER: Minimizes length by trimming leading zeros from R and S.
  • Base64 Encoding: Converts binary signatures to text-safe ASCII.

DER Format and Its History

The Distinguished Encoding Rules (DER) is a subset of ASN.1 (Abstract Syntax Notation One), standardized for encoding structured data in a compact, unambiguous binary format. DER is widely used in cryptography for encoding signatures, certificates, and keys.

Origins and Purpose

Developed in the 1980s by the ITU-T and ISO/IEC, ASN.1 and DER emerged to standardize data exchange in telecommunications and computing. DER’s strict rules ensure a single, canonical encoding for each data structure, critical for security applications where ambiguity could lead to vulnerabilities.


Why DER?

  • Unambiguity: Ensures identical data has one encoding, vital for signature verification.
  • Interoperability: Used in X.509 certificates, SSL/TLS, and blockchain, enabling cross-platform compatibility.
  • Structure: Supports complex data (e.g., SEQUENCE of INTEGERs for ECDSA signatures).

Necessity in Blockchain: In blockchain, DER is used (e.g., by Bitcoin, Ethereum, Java) to encode ECDSA signatures, ensuring consistent parsing across systems. Its structured format allows verification of \( R \), \( S \) components without ambiguity, unlike RAW’s simple concatenation.


Critical Note:
DER’s canonical encoding prevents attacks exploiting encoding variations, making it a standard in cryptographic protocols.

Compact DER vs Normal DER

DER encodings for ECDSA signatures can be Compact or Normal, differing in how \( R \) and \( S \) are encoded as ASN.1 INTEGERs.

Normal DER

In Normal DER, \( R \) and \( S \) are encoded as full 32-byte integers (for secp256r1), regardless of leading zeros, unless the MSB requires a \( 0x00 \) for positivity:

  • Length: Typically 70-72 bytes (32 or 33 bytes per INTEGER).
  • Example: An \( R \) with leading zeros (e.g., `0000...1234`) is encoded as 32 bytes.
  • Use Case: Common in platforms prioritizing simplicity over size (e.g., some C# implementations).

Example (R = `0000...3082DA1F...`, 32 bytes):


304502203082DA1FE652B2EDCC2EE3E20E18B07BE1430E4C7763482B27E914697FDE9BF5022100CCBFC7B09337E29A5D243364DC80086FA1425FFEF66A34143487D103247592C4
- rLength = 0x20 (32 bytes)
- sLength = 0x21 (33 bytes, with 0x00)

                

Compact DER

Compact DER trims leading zeros from \( R \) and \( S \), encoding only the minimal bytes needed to represent the integer, unless a \( 0x00 \) is required:

  • Length: As low as 68 bytes (e.g., 16 bytes for \( R \), 33 for \( S \)).
  • Example: \( R = 0000...1234 \) becomes `1234` (e.g., 16 bytes).
  • Use Case: Preferred in Java, Bitcoin, and size-sensitive applications.

Example (same R, trimmed to 16 bytes):


304402103082DA1FE652B2EDCC2EE3E20E18B07BE1430E4C7763482B27E914697FDE9BF5022100CCBFC7B09337E29A5D243364DC80086FA1425FFEF66A34143487D103247592C4
- rLength = 0x10 (16 bytes)
- sLength = 0x21 (33 bytes)

                

Key Differences

  • Size: Compact DER is smaller (e.g., 68 vs 71 bytes).
  • Encoding: Compact trims leading zeros; Normal uses full 32 bytes.
  • Compatibility: Both are valid DER, but Compact is standard in Java, Normal in some .NET implementations.
  • Conversion: Converters must handle both, trimming for Compact DER, padding for RAW.

Critical Note:
Compact DER optimizes bandwidth in blockchain transactions, but converters must support both forms for interoperability.

Mathematical Conversion Between RAW and DER

ECDSA signatures for secp256r1 consist of \( R \), \( S \), each 32 bytes in RAW. Conversion handles variable-length DER encodings.

RAW Format

RAW concatenates:


\[ \text{RAW} = R \parallel S \]
\[ \text{Length} = 64 \text{ bytes} \]

                

Example:


R = 000000000000000000000000000000003082DA1FE652B2EDCC2EE3E20E18B07BE1430E4C7763482B27E914697FDE9BF5
S = CCBFC7B09337E29A5D243364DC80086FA1425FFEF66A34143487D103247592C4
RAW = 000000000000000000000000000000003082DA1FE652B2EDCC2EE3E20E18B07BE1430E4C7763482B27E914697FDE9BF5CCBFC7B09337E29A5D243364DC80086FA1425FFEF66A34143487D103247592C4

                

DER Format

DER is a SEQUENCE:


\[ \text{DER} = 0x30 \parallel \text{length} \parallel 0x02 \parallel \text{rLength} \parallel R \parallel 0x02 \parallel \text{sLength} \parallel S \]

                

Components:

  • \( 0x30 \): SEQUENCE tag.
  • \( \text{length} \): Total length.
  • \( \text{rLength}, \text{sLength} \): 1-33 bytes.
  • \( R, S \): Minimal bytes, \( 0x00 \) if MSB \( \geq 0x80 \).

Example:


DER = 304402103082DA1FE652B2EDCC2EE3E20E18B07BE1430E4C7763482B27E914697FDE9BF5022100CCBFC7B09337E29A5D243364DC80086FA1425FFEF66A34143487D103247592C4

                

Conversion Math

RAW to DER:

  1. Split: \( R = \text{bytes}[0:32] \), \( S = \text{bytes}[32:64] \).
  2. Trim leading zeros, keeping 1 byte.
  3. Prepend \( 0x00 \) if MSB \( \geq 0x80 \).
  4. Build: \( 0x30 \parallel \text{totalLength} \parallel 0x02 \parallel \text{rLength} \parallel R \parallel 0x02 \parallel \text{sLength} \parallel S \).

DER to RAW:

  1. Parse: Verify \( 0x30 \), extract \( R \), \( S \).
  2. Normalize: Remove \( 0x00 \) if \( \text{length} > 32 \), pad to 32 bytes.
  3. Output: \( R \parallel S \).

Critical Note:
Trimming ensures Compact DER, while padding ensures RAW’s fixed 64 bytes.

Pseudo-Code for Conversion

RAW to DER


FUNCTION RAW_TO_DER(rawSignature):
    IF LEN(rawSignature) != 64 THEN ERROR "Raw signature must be 64 bytes"
    R = rawSignature[0:32]
    S = rawSignature[32:64]
    
    // Trim leading zeros, keep at least 1 byte
    rTrimmed = TRIM_LEADING_ZEROS(R)
    IF LEN(rTrimmed) = 0 THEN rTrimmed = [0x00]
    sTrimmed = TRIM_LEADING_ZEROS(S)
    IF LEN(sTrimmed) = 0 THEN sTrimmed = [0x00]
    
    // Add 0x00 if MSB >= 0x80
    rDer = IF rTrimmed[0] >= 0x80 THEN (0x00 || rTrimmed) ELSE rTrimmed
    sDer = IF sTrimmed[0] >= 0x80 THEN (0x00 || sTrimmed) ELSE sTrimmed
    
    totalLength = 2 + LEN(rDer) + 2 + LEN(sDer)
    RETURN 0x30 || totalLength || 0x02 || LEN(rDer) || rDer || 0x02 || LEN(sDer) || sDer
END

                

DER to RAW


FUNCTION DER_TO_RAW(derSignature):
    IF derSignature[0] != 0x30 THEN ERROR "Invalid DER"
    rStart = 4
    IF derSignature[2] != 0x02 THEN ERROR "Invalid R marker"
    rLength = derSignature[3]
    IF rLength > 33 OR rLength < 1 THEN ERROR "R length out of bounds"
    R = derSignature[rStart:rStart+rLength]
    IF R[0] = 0x00 AND rLength > 32 THEN R = R[1:]
    R = PAD_LEFT(R, 32, 0x00)
    
    sStart = rStart + rLength + 2
    IF derSignature[sStart-2] != 0x02 THEN ERROR "Invalid S marker"
    sLength = derSignature[sStart-1]
    IF sLength > 33 OR sLength < 1 THEN ERROR "S length out of bounds"
    S = derSignature[sStart:sStart+sLength]
    IF S[0] = 0x00 AND sLength > 32 THEN S = S[1:]
    S = PAD_LEFT(S, 32, 0x00)
    
    RETURN R || S
END

                

Critical Note:
Compact DER output ensures interoperability with Java-based systems.

Why Use Base64 Encoding?

Signatures are binary, risking corruption in text-based systems. Base64 converts them to ASCII:

  • Text Safety: Safe for JSON, HTTP, databases.
  • Efficiency: ~33% size increase.
  • Standardization: Universal support.

Example:


RAW (64 bytes): 0000000000000000...247592C4
Base64: AAAAAAAAAAAA...QMkdZLE=
DER (70 bytes): 304402103082DA1F...247592C4
Base64: MEYCIQDyEdof5lKy...QMkdZLE=

                

Critical Note:
Base64 ensures safe transmission across platforms.

C# Implementation

Below are C# functions for converting ECDSA signatures between RAW and DER formats for secp256r1.

RAW to DER


using System;
using System.Linq;

public byte[] RawToDerSignature(byte[] rawSignature)
{
    if (rawSignature.Length != 64)
        throw new ArgumentException("Raw signature must be 64 bytes for secp256r1");

    byte[] r = rawSignature.Take(32).ToArray();
    byte[] s = rawSignature.Skip(32).Take(32).ToArray();

    // Trim leading zeros, keep at least 1 byte
    int rTrimmedLength = r.Length;
    while (rTrimmedLength > 1 && r[r.Length - rTrimmedLength] == 0x00)
        rTrimmedLength--;
    byte[] rTrimmed = r.Skip(r.Length - rTrimmedLength).Take(rTrimmedLength).ToArray();

    int sTrimmedLength = s.Length;
    while (sTrimmedLength > 1 && s[s.Length - sTrimmedLength] == 0x00)
        sTrimmedLength--;
    byte[] sTrimmed = s.Skip(s.Length - sTrimmedLength).Take(sTrimmedLength).ToArray();

    // Add 0x00 if MSB >= 0x80
    bool rNeedsPadding = rTrimmed[0] >= 0x80;
    bool sNeedsPadding = sTrimmed[0] >= 0x80;

    byte[] rDer = rNeedsPadding ? new byte[] { 0x00 }.Concat(rTrimmed).ToArray() : rTrimmed;
    byte[] sDer = sNeedsPadding ? new byte[] { 0x00 }.Concat(sTrimmed).ToArray() : sTrimmed;

    int totalLength = 2 + rDer.Length + 2 + sDer.Length;
    byte[] der = new byte[2 + totalLength];
    der[0] = 0x30;
    der[1] = (byte)totalLength;
    der[2] = 0x02;
    der[3] = (byte)rDer.Length;
    Buffer.BlockCopy(rDer, 0, der, 4, rDer.Length);
    der[4 + rDer.Length] = 0x02;
    der[5 + rDer.Length] = (byte)sDer.Length;
    Buffer.BlockCopy(sDer, 0, der, 6 + rDer.Length, sDer.Length);

    return der;
}

                

DER to RAW


public byte[] DerToRawSignature(byte[] derSignature)
{
    if (derSignature[0] != 0x30 || derSignature.Length < 6)
        throw new ArgumentException("Invalid DER signature format");

    int rStart = 4;
    if (derSignature[2] != 0x02)
        throw new ArgumentException("Invalid R marker");
    int rLength = derSignature[3];
    if (rLength < 1 || rLength > 33 || rStart + rLength > derSignature.Length)
        throw new ArgumentException("R length out of bounds for secp256r1");

    byte[] rFull = new byte[rLength];
    Buffer.BlockCopy(derSignature, rStart, rFull, 0, rLength);

    byte[] r = new byte[32];
    int rSrcOffset = rLength > 32 ? 1 : 0;
    int rBytesToCopy = Math.Min(rLength, 32);
    int rDestOffset = 32 - rBytesToCopy;
    Buffer.BlockCopy(rFull, rSrcOffset, r, rDestOffset, rBytesToCopy);

    int sStart = rStart + rLength + 2;
    if (derSignature[sStart - 2] != 0x02)
        throw new ArgumentException("Invalid S marker");
    int sLength = derSignature[sStart - 1];
    if (sLength < 1 || sLength > 33 || sStart + sLength > derSignature.Length)
        throw new ArgumentException("S length out of bounds for secp256r1");

    byte[] sFull = new byte[sLength];
    Buffer.BlockCopy(derSignature, sStart, sFull, 0, sLength);

    byte[] s = new byte[32];
    int sSrcOffset = sLength > 32 ? 1 : 0;
    int sBytesToCopy = Math.Min(sLength, 32);
    int sDestOffset = 32 - sBytesToCopy;
    Buffer.BlockCopy(sFull, sSrcOffset, s, sDestOffset, sBytesToCopy);

    return r.Concat(s).ToArray();
}

                

Sample Usage and Outputs

Using your sample signature:


public static void Main()
{
    // Sample RAW signature (64 bytes)
    byte[] rawSignature = new byte[] {
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x30, 0x82, 0xDA, 0x1F, 0xE6, 0x52, 0xB2, 0xED, 0xCC, 0x2E, 0xE3, 0xE2, 0x0E, 0x18, 0xB0, 0x7B,
        0xCC, 0xBF, 0xC7, 0xB0, 0x93, 0x37, 0xE2, 0x9A, 0x5D, 0x24, 0x33, 0x64, 0xDC, 0x80, 0x08, 0x6F,
        0xA1, 0x42, 0x5F, 0xFE, 0xF6, 0x6A, 0x34, 0x14, 0x34, 0x87, 0xD1, 0x03, 0x24, 0x75, 0x92, 0xC4
    };

    // Convert RAW to DER
    byte[] derSignature = RawToDerSignature(rawSignature);
    string derBase64 = Convert.ToBase64String(derSignature);
    Console.WriteLine("DER Signature (Base64): " + derBase64);

    // Convert DER back to RAW
    byte[] rawBack = DerToRawSignature(derSignature);
    string rawBase64 = Convert.ToBase64String(rawBack);
    Console.WriteLine("RAW Signature (Base64): " + rawBase64);
}

                

Sample Input:
RAW Signature (hex): 000000000000000000000000000000003082DA1FE652B2EDCC2EE3E20E18B07BCCBFC7B09337E29A5D243364DC80086FA1425FFEF66A34143487D103247592C4

Sample Output:
DER Signature (Base64): MEYCIQDyEdof5lKy7cwu4+IOGLB74UMOTHdjSCsn6RRpf96b9QIhAIy/x7CTN+KaXSQzZNyACG+hQl/+/mq0FDSH0QMkdZLE=
RAW Signature (Base64): AAAAAAAAAAAAAAAAAAAAAAAAAAAAAzCC2h/mUrLtzC7j4g4YsHvBQw5Mvx7wkzfinl0kM2TcgAhvoUJf/vZqNBQ0h9EDJHWSxA==

Critical Note:
The converters are reversible, producing Compact DER for interoperability with Java.

Additional Resources


Acknowledgments

Special thanks to Grok, for its assistance in developing this tutorial on ECDSA signature conversion.