Skip to content
ecdsa.com

Convert DER ⇄ raw · Java

How to convert an ECDSA signature between DER and raw in Java

To convert an ECDSA signature between DER and raw in Java you need no library: the JDK has no public method for it, but the DER structure is just a SEQUENCE of two INTEGERs, and java.math.BigInteger does the integer encoding for you. This recipe converts in both directions with the plain JDK — and shows when the P1363 algorithm names make conversion unnecessary.

Tested with OpenJDK 25.0.4.1 (Eclipse Temurin 25.0.4.1+1, SunEC provider).

SignatureFormats.java — DER ⇄ r‖s with ByteBuffer and BigInteger
import java.io.ByteArrayOutputStream;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.Arrays;
final class SignatureFormats {
/** DER SEQUENCE { INTEGER r, INTEGER s } to r‖s; size: 32 P-256, 48 P-384, 66 P-521. */
static byte[] derToRaw(byte[] der, int size) {
ByteBuffer in = ByteBuffer.wrap(der);
if (in.get() != 0x30) throw new IllegalArgumentException("not a DER SEQUENCE");
int length = in.get() & 0xff;
if (length == 0x81) length = in.get() & 0xff; // long form: P-521 signatures only
if (length != in.remaining()) throw new IllegalArgumentException("bad SEQUENCE length");
byte[] raw = new byte[2 * size];
writeFixed(readInteger(in), raw, 0, size);
writeFixed(readInteger(in), raw, size, size);
if (in.hasRemaining()) throw new IllegalArgumentException("trailing bytes");
return raw;
}
/** r‖s to DER; BigInteger.toByteArray() already is the minimal DER INTEGER body. */
static byte[] rawToDer(byte[] raw) {
int half = raw.length / 2;
byte[] r = new BigInteger(1, Arrays.copyOfRange(raw, 0, half)).toByteArray();
byte[] s = new BigInteger(1, Arrays.copyOfRange(raw, half, raw.length)).toByteArray();
int length = 2 + r.length + 2 + s.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(0x30);
if (length > 127) out.write(0x81);
out.write(length);
out.write(0x02);
out.write(r.length);
out.writeBytes(r);
out.write(0x02);
out.write(s.length);
out.writeBytes(s);
return out.toByteArray();
}
private static BigInteger readInteger(ByteBuffer in) {
if (in.get() != 0x02) throw new IllegalArgumentException("expected INTEGER");
byte[] value = new byte[in.get() & 0xff];
in.get(value);
return new BigInteger(value);
}
private static void writeFixed(BigInteger value, byte[] out, int offset, int size) {
byte[] bytes = value.toByteArray(); // may start with a 0x00 sign byte
int skip = bytes[0] == 0 ? 1 : 0;
int length = bytes.length - skip;
if (value.signum() <= 0 || length > size) throw new IllegalArgumentException("integer does not fit the curve");
System.arraycopy(bytes, skip, out, offset + size - length, length); // left-pad with zeros
}
}
convert the shared example signature and check the round trip
byte[] der = Files.readAllBytes(Path.of("sig.der"));
byte[] raw = SignatureFormats.derToRaw(der, 32); // 64 bytes: r‖s
Files.write(Path.of("sig.raw"), raw);
System.out.println("raw: " + HexFormat.of().formatHex(raw));
System.out.println("round trip: " + Arrays.equals(SignatureFormats.rawToDer(raw), der));
skip conversion: choose the encoding by algorithm name
// Often no conversion is needed: the JDK reads and writes both encodings
Signature derVerifier = Signature.getInstance("SHA256withECDSA"); // DER
Signature rawVerifier = Signature.getInstance("SHA256withECDSAinP1363Format"); // r‖s

How it works

  1. A DER ECDSA signature is 30 len 02 len r 02 len s. derToRaw walks that layout with a ByteBuffer and rejects anything else — a wrong tag, a length that does not match, trailing bytes. A truncated input stops with BufferUnderflowException.
  2. Each INTEGER is two's complement, so it can carry a leading 0x00 when its top bit is set, and it is shorter than 32 bytes when r or s happens to be small. writeFixed strips the sign byte and left-pads to the curve size.
  3. For the reverse direction, new BigInteger(1, half).toByteArray() returns exactly the minimal two's complement bytes a DER INTEGER must contain, sign byte included.
  4. P-521 signatures exceed 127 bytes, so their SEQUENCE length needs the long form 0x81 len. Both directions handle it; the run above round-trips 1,000 fresh P-521 signatures.

Gotchas

  • Copying toByteArray() straight into a 32-byte slot breaks in both directions: it is 33 bytes when the top bit is set and shorter when the value is small. The run includes two real signatures with a 31-byte r and a 31-byte s for that reason.
  • new BigInteger(bytes) reads two's complement. A raw half whose top bit is set becomes a negative number and encodes to an invalid DER INTEGER — use the new BigInteger(1, bytes) form for raw halves.
  • size must match the curve: 32 for P-256, 48 for P-384, 66 for P-521. A P-384 signature converted with 32 fails with "integer does not fit the curve" instead of being silently truncated.
  • Since JDK 9 most conversions are avoidable: SHA256withECDSAinP1363Format signs and verifies r‖s directly. Convert only where an API dictates the format, such as a hardware token or a library that accepts DER alone.

Related recipes