import java.security.KeyFactoryimport java.security.PublicKeyimport java.security.Signatureimport java.security.spec.X509EncodedKeySpecimport kotlin.io.encoding.Base64import kotlin.io.path.Pathimport kotlin.io.path.readBytesimport 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")} import java.security.PublicKeyimport java.security.Signatureimport 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
Base64.Mimefromkotlin.io.encodingdecodes the PEM body including its line breaks;X509EncodedKeySpecis the JDK's name for SPKI, the content ofBEGIN PUBLIC KEY.Signature.getInstance("SHA256withECDSA").run { … }keeps the init-update-verify sequence in one expression. The JDK hashes the message, soupdategets the original bytes.SHA256withECDSAreads DER only. The extension function switches toSHA256withECDSAinP1363Formatfor 64-byte signatures, the fixedr‖sform of P-256.- The extension returns
falsefor 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
verifythrowsSignatureExceptionfor 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.Defaultrejects the line breaks inside a PEM body (Invalid symbol '\n'). UseBase64.Mime, or strip whitespace first.- A
Signatureobject keeps state betweenupdateandverify. Never share one instance between coroutines or threads;getInstanceper 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…orSHA512…algorithm names.