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.
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.
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:
- Split: \( R = \text{bytes}[0:32] \), \( S = \text{bytes}[32:64] \).
- Trim leading zeros, keeping 1 byte.
- Prepend \( 0x00 \) if MSB \( \geq 0x80 \).
- Build: \( 0x30 \parallel \text{totalLength} \parallel 0x02 \parallel \text{rLength} \parallel R \parallel 0x02 \parallel \text{sLength} \parallel S \).
DER to RAW:
- Parse: Verify \( 0x30 \), extract \( R \), \( S \).
- Normalize: Remove \( 0x00 \) if \( \text{length} > 32 \), pad to 32 bytes.
- Output: \( R \parallel S \).
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
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=
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);
}
RAW Signature (hex): 000000000000000000000000000000003082DA1FE652B2EDCC2EE3E20E18B07BCCBFC7B09337E29A5D243364DC80086FA1425FFEF66A34143487D103247592C4
DER Signature (Base64): MEYCIQDyEdof5lKy7cwu4+IOGLB74UMOTHdjSCsn6RRpf96b9QIhAIy/x7CTN+KaXSQzZNyACG+hQl/+/mq0FDSH0QMkdZLE=
RAW Signature (Base64): AAAAAAAAAAAAAAAAAAAAAAAAAAAAAzCC2h/mUrLtzC7j4g4YsHvBQw5Mvx7wkzfinl0kM2TcgAhvoUJf/vZqNBQ0h9EDJHWSxA==
The converters are reversible, producing Compact DER for interoperability with Java.
Additional Resources
Reference Implementations
Acknowledgments
Special thanks to Grok, for its assistance in developing this tutorial on ECDSA signature conversion.