Hello, guys! I'm having trouble to validate JWT with EC algorithm in iOS. I have generated JWT and public key from jwt.io using the ES384 algorithm and I have the following validator:
let jwtToken = "eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.VUPWQZuClnkFbaEKCsPy7CZVMh5wxbCSpaAWFLpnTe9J0--PzHNeTFNXCrVHysAa3eFbuzD8_bLSsgTKC8SzHxRVSj5eN86vBPo_1fNfE7SHTYhWowjY4E_wuiC13yoj"
let publicKey = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEC1uWSXj2czCDwMTLWV5BFmwxdM6PX9p+Pk9Yf9rIf374m5XP1U8q79dBhLSIuaojsvOT39UUcPJROSD1FqYLued0rXiooIii1D3jaW6pmGVJFhodzC31cy5sfOYotrzF"
let isValid = JWTValidator.validateSignature(forToken: jwtToken, withPublicKey: publicKey)
print(isValid) // always false
import ASN1Decoder
class JWTValidator {
static func validateSignature(forToken token: String, withPublicKey publicKeyText: String) -> Bool {
let parts = token.components(separatedBy: ".")
let header = parts[0]
let payload = parts[1]
let signature = parts[2]
guard let dataPublicKey = Data(base64Encoded: publicKeyText),
let dataSigned = (header + "." + payload).data(using: .ascii),
let dataSignature = Data(base64Encoded: base64StringWithPadding(base64str: signature)) else {
print("Failed to get signature!")
return false
}
var secKeyCreateError : Unmanaged<CFError>?
guard let publicKey: SecKey = DerDecoder().decodePublicKey(dataPublicKey,&secKeyCreateError) else {
print("Failed to create SecKey : %@", secKeyCreateError!.takeRetainedValue().localizedDescription)
return false
}
var validateError : Unmanaged<CFError>?
let algorithm: SecKeyAlgorithm = .ecdsaSignatureMessageX962SHA384
let result = SecKeyVerifySignature(peerPublicKey,
algorithm,
dataSigned as NSData,
dataSignature as NSData,
&validateError)
if let validateError = validateError {
print(validateError)
}
return result
}
static func base64StringWithPadding(base64str: String) -> String {
var newStr = base64str.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
let count = newStr.count % 4
if count > 0 {
let amount = 4 - count
for _ in 0..<amount {
newStr += "="
}
}
return newStr
}
}
class DerDecoder {
func decodePublicKey(_ data: Data, _ error: UnsafeMutablePointer<Unmanaged<CFError>?>?) -> SecKey? {
guard
let asn1 = try? ASN1DERDecoder.decode(data: data),
let keyData = asn1.first?.sub(1)?.value as? Data
else {
return nil
}
return SecKeyCreateWithData(
keyData as CFData,
[
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrKeyClass as String: kSecAttrKeyClassPublic,
] as CFDictionary,
error
)
}
}
Initially I had some troubles to get the SecKey, but the ASN1Decoder solved that issue. Now I'm struggling to get it validated. I'm always getting false
. I don't know what SecKeyAlgorithm
to use.
These are the errors I'm getting:
EC signature verification failed (ccerr -7)
or
algorithm not supported by the key <SecKeyRef curve type: kSecECCurveSecp384r1, algorithm id: 3, key type: ECPublicKey, version: 4, block size: 384 bits, y: B2F393DFD51470F2513920F516A60BB9E774AD78A8A088A2D43DE3696EA9986549161A1DCC2DF5732E6C7CE628B6BCC5, x: 0B5B964978F6733083C0C4CB595E41166C3174CE8F5FDA7E3E4F587FDAC87F7EF89B95CFD54F2AEFD74184B488B9AA23, addr: 0x102e26140>
Any help will be highly appreciated!
Edit
If I use RS256 algorithm from jwt.io, without ASN1Decoder and
let algorithm: SecKeyAlgorithm = .rsaSignatureMessagePKCS1v15SHA256
there's no issue at all.
Thanks!
With a few tweaks I was able to get your code working. My version is pasted in below. I tested it on macOS 13.2.1.
The issues with your code are:
-
You have the wrong algorithm. You need
.ecdsaSignatureMessageX962SHA384
. -
That also means you need to convert the raw signature into X9.62 format. I used
P384.Signing.ECDSASignature
to do this, see the code arounddataSignatureX962
, but I kinda suspect that you want to avoid a dependency on CryptoKit, in which case you’ll need to do you own ASN.1 munging.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
import Foundation
import CryptoKit
class JWTValidator {
static func validateSignature() {
let jwtToken = "eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.VUPWQZuClnkFbaEKCsPy7CZVMh5wxbCSpaAWFLpnTe9J0--PzHNeTFNXCrVHysAa3eFbuzD8_bLSsgTKC8SzHxRVSj5eN86vBPo_1fNfE7SHTYhWowjY4E_wuiC13yoj"
let publicKeyBase64 = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEC1uWSXj2czCDwMTLWV5BFmwxdM6PX9p+Pk9Yf9rIf374m5XP1U8q79dBhLSIuaojsvOT39UUcPJROSD1FqYLued0rXiooIii1D3jaW6pmGVJFhodzC31cy5sfOYotrzF"
let parts = jwtToken.components(separatedBy: ".")
let header = parts[0]
let payload = parts[1]
let signature = parts[2]
let dataPublicKey = Data(base64Encoded: publicKeyBase64)!
let dataSigned = (header + "." + payload).data(using: .ascii)!
let dataSignature = Data(base64Encoded: base64StringWithPadding(base64str: signature))!
let dataSignatureX962 = try! P384.Signing.ECDSASignature(rawRepresentation: dataSignature).derRepresentation
if #available(iOS 14.0, *) {
let ck = try! P384.Signing.PublicKey(derRepresentation: dataPublicKey)
let x963 = ck.x963Representation
let publicKey = SecKeyCreateWithData(x963 as NSData, [
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrKeyClass as String: kSecAttrKeyClassPublic,
] as NSDictionary, nil)!
print(publicKey)
// we have the SecKey representation of the public key.
// validate the JWT with the public key.
var validateError : Unmanaged<CFError>?
let algorithm: SecKeyAlgorithm = .ecdsaSignatureMessageX962SHA384
let result = SecKeyVerifySignature(publicKey,
algorithm,
dataSigned as NSData,
dataSignatureX962 as NSData,
&validateError)
if let validateError = validateError {
print(validateError)
}
print("JWT is valid: \(result)") // prints JWT is valid: false
}
}
static func base64StringWithPadding(base64str: String) -> String {
var newStr = base64str.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
let count = newStr.count % 4
if count > 0 {
let amount = 4 - count
for _ in 0..<amount {
newStr += "="
}
}
return newStr
}
}
JWTValidator.validateSignature()