Skip to content

Instantly share code, notes, and snippets.

@WorldDownTown
Last active December 2, 2024 08:11
Show Gist options
  • Save WorldDownTown/7f1d6c3c66e477545f77f10ef9ae798b to your computer and use it in GitHub Desktop.
Save WorldDownTown/7f1d6c3c66e477545f77f10ef9ae798b to your computer and use it in GitHub Desktop.
ID Token verification for RS256
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