Skip to content
ecdsa.com

PEM ⇄ JWK · Swift

How to convert a PEM public key to a JWK and back in Swift

Swift converts a P-256 PEM public key to a JWK and back with the CryptoKit API (swift-crypto on Linux): P256.Signing.PublicKey reads the PEM, its rawRepresentation holds x and y, and init(rawRepresentation:) plus pemRepresentation go the other way. A small Codable struct is the JWK. The run checks the RFC 7515 round trip byte for byte.

Tested with swift-crypto 4.5.2 (Swift 6.4.1, Linux).

Package.swift — swift-crypto (CryptoKit on Apple platforms)
// swift-tools-version:6.0
import PackageDescription
let package = Package(
name: "pem-jwk-demo",
dependencies: [
.package(url: "https://github.com/apple/swift-crypto.git", exact: "4.5.2"),
],
targets: [
.executableTarget(name: "pem-jwk-demo", dependencies: [.product(name: "Crypto", package: "swift-crypto")]),
]
)
main.swift — PEM to JWK and JWK to PEM for a P-256 key
import Crypto
import Foundation
struct ECPublicJWK: Codable {
var kty = "EC"
var crv = "P-256"
var x: String
var y: String
}
func base64URL(_ data: Data) -> String {
data.base64EncodedString()
.replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "=", with: "")
}
func base64URLDecode(_ string: String) -> Data? {
var base64 = string.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
base64 += String(repeating: "=", count: (4 - base64.count % 4) % 4)
return Data(base64Encoded: base64)
}
// PEM -> JWK: rawRepresentation is x ‖ y, 32 bytes each
let key = try P256.Signing.PublicKey(pemRepresentation: String(contentsOfFile: "pub.pem", encoding: .utf8))
let point = key.rawRepresentation
let jwk = ECPublicJWK(x: base64URL(point.prefix(32)), y: base64URL(point.suffix(32)))
let json = try JSONEncoder().encode(jwk)
try json.write(to: URL(fileURLWithPath: "out.jwk.json"))
print(String(decoding: json, as: UTF8.self))
// JWK -> PEM: init(rawRepresentation:) validates the point, pemRepresentation writes SPKI
let input = try JSONDecoder().decode(ECPublicJWK.self, from: Data(contentsOf: URL(fileURLWithPath: "pub.jwk.json")))
guard input.kty == "EC", input.crv == "P-256", let x = base64URLDecode(input.x), let y = base64URLDecode(input.y) else {
fatalError("not a P-256 public JWK")
}
let pem = try P256.Signing.PublicKey(rawRepresentation: x + y).pemRepresentation + "\n"
try pem.write(toFile: "out.pem", atomically: true, encoding: .utf8)
print(pem, terminator: "")

How it works

  1. P256.Signing.PublicKey(pemRepresentation:) parses BEGIN PUBLIC KEY.
  2. rawRepresentation is the 64-byte concatenation x ‖ y; each half, base64url-encoded, is a JWK coordinate.
  3. ECPublicJWK is a Codable struct; JSONEncoder writes it and JSONDecoder reads pub.jwk.json.
  4. P256.Signing.PublicKey(rawRepresentation: x + y) checks the point, and pemRepresentation writes the SPKI PEM.

Gotchas

  • pemRepresentation has no final newline; the recipe adds one so the file matches what OpenSSL writes.
  • JSONEncoder does not guarantee member order and escapes /. That is fine for a JWK (no / in base64url, order irrelevant), but not for anything that is hashed, such as an RFC 7638 thumbprint.
  • P256.KeyAgreement.PublicKey and P256.Signing.PublicKey are different types for the same bytes; pick the one matching the key's use, since converting between them requires a round trip through rawRepresentation.
  • Curve25519 keys have no PEM support in CryptoKit; for Ed25519, add or strip the 12-byte SPKI header yourself, as the Ed25519 recipe of this series shows.

Related recipes