Skip to content
ecdsa.com

Convert DER ⇄ raw · Kotlin

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

To convert an ECDSA signature between DER and raw in Kotlin, two small ByteArray extension functions are enough — no library, because DER here is only a SEQUENCE of two INTEGERs. The recipe handles the cases naive converters miss: sign bytes, 31-byte integers and the long-form length of P-521 signatures.

Tested with Kotlin 2.4.21 (Kotlin/JVM, OpenJDK 25.0.4.1 (Temurin), Gradle 9.8.1).

SignatureFormats.kt — derToRaw and rawToDer as ByteArray extensions
import java.math.BigInteger
/** DER SEQUENCE { INTEGER r, INTEGER s } to fixed-width r‖s; size: 32 P-256, 48 P-384, 66 P-521. */
fun ByteArray.derToRaw(size: Int): ByteArray {
var pos = 0
fun next(): Int = this[pos++].toInt() and 0xff // Byte is signed in Kotlin
require(next() == 0x30) { "not a DER SEQUENCE" }
var length = next()
if (length == 0x81) length = next() // long form: P-521 only
require(length == this.size - pos) { "bad SEQUENCE length" }
fun integer(): ByteArray {
require(next() == 0x02) { "expected INTEGER" }
val len = next()
val value = copyOfRange(pos, pos + len).dropWhile { it == 0.toByte() }.toByteArray()
pos += len
require(value.size <= size) { "integer does not fit the curve" }
return ByteArray(size - value.size) + value // left-pad with zeros
}
val raw = integer() + integer()
require(pos == this.size) { "trailing bytes" }
return raw
}
/** Fixed-width r‖s to DER; BigInteger.toByteArray() is already a minimal DER INTEGER body. */
fun ByteArray.rawToDer(): ByteArray {
val half = size / 2
fun integer(bytes: ByteArray): ByteArray {
val value = BigInteger(1, bytes).toByteArray()
return byteArrayOf(0x02, value.size.toByte()) + value
}
val body = integer(copyOfRange(0, half)) + integer(copyOfRange(half, size))
val length = if (body.size > 127) byteArrayOf(0x81.toByte(), body.size.toByte()) else byteArrayOf(body.size.toByte())
return byteArrayOf(0x30) + length + body
}
Main.kt — convert the shared example signature and check the round trip
fun main() {
val der = "30450221009f4118b059a34a2f344c39a1d085d93bab560402938281dc1cb2f3dfb8eee58602200bd54b34a72042551c3316e10f461a1d69036ba4f0d72d95d42465340b56def8".hexToByteArray()
val raw = der.derToRaw(32)
println("raw: ${raw.toHexString()}")
println("round trip: ${raw.rawToDer().contentEquals(der)}") // not ==, which compares references
}

How it works

  1. derToRaw reads the tag-length-value layout 30 len 02 len r 02 len s, rejecting a wrong tag, a mismatched length or trailing bytes.
  2. Each INTEGER may start with a 0x00 sign byte and may be shorter than the curve size. dropWhile strips leading zeros and ByteArray(size - value.size) + value left-pads to the fixed width.
  3. rawToDer relies on BigInteger(1, bytes).toByteArray(), which returns exactly the minimal two's complement form a DER INTEGER must contain.
  4. Kotlin 2.2+ has hexToByteArray() and toHexString() in the standard library, so hex test data needs no helper.

Gotchas

  • Byte is signed in Kotlin: this[pos].toInt() is negative for bytes from 0x80 up, so length and tag comparisons silently fail. Mask with and 0xff, as next() does.
  • raw.rawToDer() == der is always false — == on arrays compares references. Use contentEquals.
  • Converters that copy BigInteger.toByteArray() straight into 32 bytes break on the 33-byte (sign byte) and 31-byte cases. The run converts two real signatures with a 31-byte r and a 31-byte s, then 1,000 fresh signatures on P-256 and P-521.
  • On the JDK you often do not need to convert: SHA256withECDSAinP1363Format signs and verifies r‖s directly. Conversion is for APIs that insist on one format, such as hardware tokens or DER-only libraries.

Related recipes