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 }} byte[] der = Files.readAllBytes(Path.of("sig.der"));byte[] raw = SignatureFormats.derToRaw(der, 32); // 64 bytes: r‖sFiles.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)); // Often no conversion is needed: the JDK reads and writes both encodingsSignature derVerifier = Signature.getInstance("SHA256withECDSA"); // DERSignature rawVerifier = Signature.getInstance("SHA256withECDSAinP1363Format"); // r‖s How it works
- A DER ECDSA signature is
30 len 02 len r 02 len s.derToRawwalks that layout with aByteBufferand rejects anything else — a wrong tag, a length that does not match, trailing bytes. A truncated input stops withBufferUnderflowException. - Each INTEGER is two's complement, so it can carry a leading
0x00when its top bit is set, and it is shorter than 32 bytes when r or s happens to be small.writeFixedstrips the sign byte and left-pads to the curve size. - 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. - 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 thenew BigInteger(1, bytes)form for raw halves.sizemust 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:
SHA256withECDSAinP1363Formatsigns and verifiesr‖sdirectly. Convert only where an API dictates the format, such as a hardware token or a library that accepts DER alone.