import io.jsonwebtoken.security.Jwksimport io.jsonwebtoken.security.PublicJwkimport java.io.Fileimport java.security.KeyFactoryimport java.security.interfaces.ECPublicKeyimport java.security.spec.X509EncodedKeySpecimport java.util.Base64 fun main() { // PEM -> JWK: the JDK reads the SPKI, JJWT builds and serializes the JWK val der = Base64.getMimeDecoder().decode(File("pub.pem").readText().replace(Regex("-----[^-]+-----"), "")) val key = KeyFactory.getInstance("EC").generatePublic(X509EncodedKeySpec(der)) as ECPublicKey val json = Jwks.json(Jwks.builder().key(key).build()) File("out.jwk.json").writeText(json) println(json) // JWK -> PEM: parse the JWK, getEncoded() of its key is the SPKI DER val jwk = Jwks.parser().build().parse(File("pub.jwk.json").readText()) as PublicJwk<*> val body = Base64.getMimeEncoder(64, "\n".toByteArray()).encodeToString(jwk.toKey().encoded) val pem = "-----BEGIN PUBLIC KEY-----\n$body\n-----END PUBLIC KEY-----\n" File("out.pem").writeText(pem) print(pem)}How it works
- The PEM armour is removed and the body decoded with the MIME decoder;
X509EncodedKeySpecreads the SPKI as anECPublicKey. Jwks.builder().key(key).build()returns anEcPublicJwk;Jwks.json(jwk)writes it as JSON.Jwks.parser().build().parse(json)validates the JWK (curve, point) and returns aPublicJwkfor a public key.toKey().encodedis the SPKI DER; the MIME encoder with 64-character lines rebuilds the PEM.
Gotchas
Jwks.jsonaccepts only aPublicJwk, so a private JWK cannot be serialized with it by accident;toString()of a private JWK prints<redacted>for secret members. Other JSON serializers handed a privateJwkget no such protection.Jwks.builder().key(…)needs the key's static type: withECPublicKey(as here) the EC overload applies; with a plainPublicKey, Kotlin needs explicit type arguments.- Parsing a JWK needs
jjwt-impland a JSON module (jjwt-gsonorjjwt-jackson) at runtime, like every other JJWT operation. - The JDK's
KeyFactorycannot readBEGIN EC PRIVATE KEYor certificate PEM files; this route is forBEGIN PUBLIC KEYblocks.