Last active
December 2, 2024 08:11
-
-
Save WorldDownTown/7f1d6c3c66e477545f77f10ef9ae798b to your computer and use it in GitHub Desktop.
ID Token verification for RS256
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import Security | |
/// JWT検証 | |
/// - Parameters: | |
/// - rsaPublicKey: 公開鍵の SecKey | |
/// - signature: JWT のドット区切りの3番目 | |
/// - headerAndPayload: JWT のドット区切りの1・2番目 | |
/// - Returns: JWTの検証結果 | |
func verifyJWT(rsaPublicKey: SecKey, signature: String, headerAndPayload: String) -> Bool { | |
guard let signatureData = Data(base64URLEncoded: signature) else { return false } | |
let headerAndPayloadData: Data = .init(headerAndPayload.utf8) | |
var error: Unmanaged<CFError>? | |
/// https://mtanriverdi.medium.com/how-to-decode-jwt-and-validate-the-signature-in-swift-97092bd654f7 | |
let result: Bool = SecKeyVerifySignature( | |
rsaPublicKey, | |
.rsaSignatureMessagePKCS1v15SHA256, | |
headerAndPayloadData as CFData, | |
signatureData as CFData, | |
&error | |
) | |
if let error { | |
print(#line, "Error verifying data: \(error.takeRetainedValue())") | |
return false | |
} | |
return result | |
} | |
/// JWKからSecKeyを生成する処理 | |
func createRSAPublicKey(n: String, e: String) -> SecKey? { | |
guard let modulusData = Data(base64URLEncoded: n), | |
let exponentData = Data(base64URLEncoded: e) else { | |
print("Invalid JWK") | |
return nil | |
} | |
var modulusBytes: [UInt8] = .init(modulusData) | |
let exponentBytes: [UInt8] = .init(exponentData) | |
if let prefix = modulusBytes.first, prefix != 0 { | |
modulusBytes.insert(0, at: 0) | |
} | |
let modulusEncoded: [UInt8] = modulusBytes.encode(as: .integer) | |
let exponentEncoded: [UInt8] = exponentBytes.encode(as: .integer) | |
let sequenceEncoded: [UInt8] = (modulusEncoded + exponentEncoded).encode(as: .sequence) | |
let keyData: Data = .init(sequenceEncoded) | |
let keySize: Int = modulusData.count * 8 | |
// RSA 公開鍵の辞書を作成 | |
let attributes: [String: Any] = [ | |
kSecAttrKeyType as String: kSecAttrKeyTypeRSA, | |
kSecAttrKeyClass as String: kSecAttrKeyClassPublic, | |
kSecAttrKeySizeInBits as String: keySize, | |
kSecAttrIsPermanent as String: false | |
] | |
var error: Unmanaged<CFError>? | |
let publicKey: SecKey? = SecKeyCreateWithData( | |
keyData as CFData, | |
attributes as CFDictionary, | |
&error | |
) | |
if let error { | |
print("Error creating public key: \(error.takeRetainedValue())") | |
} | |
return publicKey | |
} | |
private extension Data { | |
init?(base64URLEncoded string: String) { | |
self.init( | |
base64Encoded: string.decodeBase64URL(), | |
options: .ignoreUnknownCharacters | |
) | |
} | |
} | |
private extension String { | |
func decodeBase64URL() -> String { | |
let paddingEquals: String = .init(repeating: "=", count: (4 - count % 4) % 4) | |
return (self + paddingEquals) | |
.replacingOccurrences(of: "-", with: "+") | |
.replacingOccurrences(of: "_", with: "/") | |
} | |
} | |
enum ASN1Type { | |
case sequence, integer | |
var tag: UInt8 { | |
switch self { | |
case .sequence: 0x30 | |
case .integer: 0x02 | |
} | |
} | |
} | |
extension [UInt8] { | |
func encode(as type: ASN1Type) -> Self { | |
[type.tag] + lengthField() + self | |
} | |
private func lengthField() -> Self { | |
guard count >= 128 else { return [UInt8(count)] } | |
// The number of bytes needed to encode count. | |
let lengthBytesCount: UInt8 = .init(log2(Double(count)) / 8 + 1) | |
// The first byte in the length field encoding the number of remaining bytes. | |
let firstLengthFieldByte: UInt8 = 128 + lengthBytesCount | |
var c: Int = count | |
let lengthField: [UInt8] = (0..<lengthBytesCount).reduce(into: []) { result, _ in | |
// Take the last 8 bits of count. | |
let lengthByte: UInt8 = .init(c & 0xff) | |
// Add them to the length field. | |
result.append(lengthByte) | |
// Delete the last 8 bits of count. | |
c >>= 8 | |
} + [firstLengthFieldByte] // Include the first byte. | |
return lengthField.reversed() | |
} | |
} | |
let n: String = "wnfD2k6iOI8IdDTKPY4J6HFOT1nKor6v2xEZ9G2n1_KtPs5-5aC8W_SvRTzXF9Ym-BeoQI5mfHSbaYafbeEDaCSVpxXja1K8n7EAlpYVGydTHgL2NLHADb-Gtkkiv8Gw9sSyea_foPW_i2YknOIyBM4A2Sxqf9VPQTSTj5zJGFtRnyQYuuTprxqj9qgZfAAhrGCizsW8bm62nH2DYORQ10rwaiY9kL4gVOPrU39vaB80YX5a2N-TRzDCzHaKlo9vSBMzysFs1WFmb9VdOLuIae1I7h50KFUIDncxv7tGrVxnYBi_etNl989JmDtDzLnPK3u4AMFEGcha52Y2QwxQeQ" | |
let e: String = "AQAB" | |
/// JWT のドット区切りの3番目 | |
let signature: String = "bzNpok6tybsHOicXvbP9Q97kKO14ei3B1DXlNa8LFiZj8rQJfnm_rATRlMFEGs1fsW5Av7srDy-2JjdEbQufHbYlUBXIJh7_sBwI_qU6NIYn2t8hcGpMnXoe2z0BtkP3CyvvTINRVxA6WwHv_Teh0nzxnaxmcOVm0ajLKT603Crtt4MNur_azADTxNxYafaQ5o7XOo9V0PMM0nVy6kqn-N3IjxBPNXqQapmxub6qzJcRsOyAjOyzK1hRAuxvX9vd9fAoBf4ycpbeTWIy7nQIeEU8kl2lTNSb9DBZrsVP7GzhFRdEMDIxctcBoqXDxBuYLuSXGlnMyfSYy0sU39VBtw" | |
/// JWT のドット区切りの1・2番目 | |
let headerAndPayload: String = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImRhMGI1ZDQyNDRjY2ZiNzViMjcwODQxNjI5NWYwNWQ1MThjYTY5MDMifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiYXRfaGFzaCI6Ijd2ajAzMklIQWdzMEdNUGxOUDFkV2ciLCJhdWQiOiI5MTQ2OTk1NzE0NS1qYjg0MnUwcmNnbTg3bTIyMDlhZWxiZnRzbDlwMzU3aS5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsInN1YiI6IjExNzQ0MjQ1MDQ0MzI0NDAzNTk1NSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhenAiOiI5MTQ2OTk1NzE0NS1qYjg0MnUwcmNnbTg3bTIyMDlhZWxiZnRzbDlwMzU3aS5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoiYm9idW5kZXJzb25AZ21haWwuY29tIiwiaWF0IjoxNDQzNzY4NzcxLCJleHAiOjE0NDM3NzIzNzF9" | |
if let publicKey = createRSAPublicKey(n: n, e: e) { | |
let result: Bool = verifyJWT( | |
rsaPublicKey: publicKey, | |
signature: signature, | |
headerAndPayload: headerAndPayload | |
) | |
print(#line, result) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment