1import Crypto
2import Foundation
3
4struct UnsupportedKey: Error {}
5
6/// RFC 7638: the required members of each key type, in lexicographic order.
7let requiredMembers = ["EC": ["crv", "kty", "x", "y"], "RSA": ["e", "kty", "n"], "OKP": ["crv", "kty", "x"], "oct": ["k", "kty"]]
8
9func base64URL(_ data: some DataProtocol) -> String {
10 Data(data).base64EncodedString()
11 .replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "=", with: "")
12}
13
14func jwkThumbprint(_ jwk: [String: String]) throws -> String {
15 guard let kty = jwk["kty"], let names = requiredMembers[kty] else { throw UnsupportedKey() }
16 // Built by hand: JSONSerialization would escape "/" and does not promise member order
17 let members = try names.map { name -> String in
18 guard let value = jwk[name] else { throw UnsupportedKey() }
19 return "\"\(name)\":\"\(value)\""
20 }
21 return base64URL(Data(SHA256.hash(data: Data("{\(members.joined(separator: ","))}".utf8))))
22}
23
24for file in ["ec.jwk.json", "rsa.jwk.json", "okp.jwk.json"] {
25 let object = try JSONSerialization.jsonObject(with: Data(contentsOf: URL(fileURLWithPath: file))) as! [String: Any]
26 let jwk = object.compactMapValues { $0 as? String }
27 print("\(file): \(try jwkThumbprint(jwk))")
28}
29
30// A P-256 key from PEM: x and y are the two halves of rawRepresentation
31let key = try P256.Signing.PublicKey(pemRepresentation: String(contentsOfFile: "ec.pub.pem", encoding: .utf8))
32let point = key.rawRepresentation // x ‖ y, 32 bytes each
33let jwk = ["kty": "EC", "crv": "P-256", "x": base64URL(point.prefix(32)), "y": base64URL(point.suffix(32))]
34print("ec.pub.pem: \(try jwkThumbprint(jwk))")