Skip to content
ecdsa.com

Verify a signature · Kotlin

How to verify an ECDSA signature in Kotlin

To verify an ECDSA signature in Kotlin on the JVM, use the JDK's own JCA classes — KeyFactory for the key, java.security.Signature for the check — plus the Kotlin standard library's Base64 for the PEM body. This recipe verifies the usual ASN.1 DER signature, then wraps both encodings in an extension function that also accepts the raw r‖s form used by JWS and WebCrypto.

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

Main.kt — verify a DER signature with the JDK provider
import java.security.KeyFactory
import java.security.PublicKey
import java.security.Signature
import java.security.spec.X509EncodedKeySpec
import kotlin.io.encoding.Base64
import kotlin.io.path.Path
import kotlin.io.path.readBytes
import kotlin.io.path.readText
fun readPublicKey(pem: String): PublicKey {
// "BEGIN PUBLIC KEY" is SPKI; the MIME decoder skips the line breaks of the PEM body
val spki = Base64.Mime.decode(pem.replace(Regex("-----[^-]+-----"), ""))
return KeyFactory.getInstance("EC").generatePublic(X509EncodedKeySpec(spki))
}
fun main() {
val publicKey = readPublicKey(Path("pub.pem").readText())
val message = Path("message.txt").readBytes()
val signature = Path("sig.der").readBytes() // ASN.1 DER
val valid = Signature.getInstance("SHA256withECDSA").run {
initVerify(publicKey)
update(message) // the message itself: the JDK hashes it
verify(signature)
}
println(if (valid) "valid" else "INVALID")
}
an extension function for DER and raw r‖s signatures
import java.security.PublicKey
import java.security.Signature
import java.security.SignatureException
/** Verifies DER or raw r‖s (JWS, WebCrypto) ES256-style signatures; false instead of exceptions. */
fun PublicKey.verifyEcdsaSha256(message: ByteArray, signature: ByteArray): Boolean {
val algorithm = if (signature.size == 64) "SHA256withECDSAinP1363Format" else "SHA256withECDSA"
return try {
Signature.getInstance(algorithm).run {
initVerify(this@verifyEcdsaSha256)
update(message)
verify(signature)
}
} catch (e: SignatureException) {
false // malformed DER: Kotlin will not force you to catch this, so do it here
}
}

How it works

  1. Base64.Mime from kotlin.io.encoding decodes the PEM body including its line breaks; X509EncodedKeySpec is the JDK's name for SPKI, the content of BEGIN PUBLIC KEY.
  2. Signature.getInstance("SHA256withECDSA").run { … } keeps the init-update-verify sequence in one expression. The JDK hashes the message, so update gets the original bytes.
  3. SHA256withECDSA reads DER only. The extension function switches to SHA256withECDSAinP1363Format for 64-byte signatures, the fixed r‖s form of P-256.
  4. The extension returns false for a malformed signature as well as for a wrong one, so callers handle a single outcome.

Gotchas

  • Kotlin has no checked exceptions: nothing reminds you that verify throws SignatureException for bytes that are not DER (a raw signature, for example). Unhandled, a bad client signature becomes a 500 instead of a 401 — catch it where you verify.
  • Base64.Default rejects the line breaks inside a PEM body (Invalid symbol '\n'). Use Base64.Mime, or strip whitespace first.
  • A Signature object keeps state between update and verify. Never share one instance between coroutines or threads; getInstance per verification is cheap.
  • The 64-byte rule works for P-256 only. For P-384 (96 bytes) and P-521 (132 bytes), switch on the curve's size and use SHA384… or SHA512… algorithm names.

Related recipes