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} 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
derToRawreads the tag-length-value layout30 len 02 len r 02 len s, rejecting a wrong tag, a mismatched length or trailing bytes.- Each INTEGER may start with a
0x00sign byte and may be shorter than the curve size.dropWhilestrips leading zeros andByteArray(size - value.size) + valueleft-pads to the fixed width. rawToDerrelies onBigInteger(1, bytes).toByteArray(), which returns exactly the minimal two's complement form a DER INTEGER must contain.- Kotlin 2.2+ has
hexToByteArray()andtoHexString()in the standard library, so hex test data needs no helper.
Gotchas
Byteis signed in Kotlin:this[pos].toInt()is negative for bytes from0x80up, so length and tag comparisons silently fail. Mask withand 0xff, asnext()does.raw.rawToDer() == deris alwaysfalse—==on arrays compares references. UsecontentEquals.- 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:
SHA256withECDSAinP1363Formatsigns and verifiesr‖sdirectly. Conversion is for APIs that insist on one format, such as hardware tokens or DER-only libraries.