Validate JWT with public key with ECDSASHA384 algorithm in iOS

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.

Accepted Reply

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 around dataSignatureX962, 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)
            
            &#x2F;&#x2F; we have the SecKey representation of the public key.
            &#x2F;&#x2F; validate the JWT with the public key.
            
            var validateError : Unmanaged&lt;CFError&gt;?
            let algorithm: SecKeyAlgorithm = .ecdsaSignatureMessageX962SHA384
            
            let result = SecKeyVerifySignature(publicKey,
                                               algorithm,
                                               dataSigned as NSData,
                                               dataSignatureX962 as NSData,
                                               &amp;validateError)
            
            if let validateError = validateError {
                print(validateError)
            }
            
            print("JWT is valid: \(result)") &#x2F;&#x2F; prints JWT is valid: false
        }
    }
    
    static func base64StringWithPadding(base64str: String) -&gt; String {
        var newStr = base64str.replacingOccurrences(of: "-", with: "+")
                    .replacingOccurrences(of: "_", with: "&#x2F;")
        let count = newStr.count % 4
        if count &gt; 0 {
          let amount = 4 - count
          for _ in 0..&lt;amount {
              newStr += "="
          }
        }
        return newStr
     }
}

JWTValidator.validateSignature()

Replies

Can you post a standalone example? Specifically:

  • I’m not sure where ASN1Decoder comes from but you don’t need it for this test. See the code at the end of this email.

  • I suspect your using the standard jwt.io test payload but it’d be good for you to include that here.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"


import Foundation
import CryptoKit

let base64 = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEC1uWSXj2czCDwMTLWV5BFmwxdM6PX9p+Pk9Yf9rIf374m5XP1U8q79dBhLSIuaojsvOT39UUcPJROSD1FqYLued0rXiooIii1D3jaW6pmGVJFhodzC31cy5sfOYotrzF"
let der = Data(base64Encoded: base64)!
let ck = try! P384.Signing.PublicKey(derRepresentation: der)
let x963 = ck.x963Representation
let publicKey = SecKeyCreateWithData(x963 as NSData, [
        kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
        kSecAttrKeyClass as String: kSecAttrKeyClassPublic,
] as NSDictionary, nil)!
print(publicKey)

Hi, Eskimo! Thank you for the quick reply!

I'm using the ASN1Decoder because we're supporting iOS 12 +.

I have the public key in SecKey format, but I'm struggling to verify the provided JWT token with that key.

SecKeyVerifySignature always returns false. What am I missing?

What am I missing?

I’ve no idea. If you post a self-contained example, I’d be happy to look at it.

because we're supporting iOS 12 +.

That’s fine. However, your self-contained example can rely on the latest stuff so it’d be best to break that dependency just for this test. Once you get the self-contained example working, you can ‘back port’ what you’ve learn to your real product.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

 If you post a self-contained example, I’d be happy to look at it.

I think I can't understand what kind of an example I need to provide. It's just the JWTValidator that I have posted in the question.

My use case is: I have a hardware bluetooth device with a JWT token. I have also the public key. The goal is to validate that JWT token with the existing public key and prove the device is the correct one.

So I'm trying to achieve this firstly with the JWT token and public key posted above (taken from the jwt.io standard ES384 example). Then I'll test with the real data from the product.

I'll be more than happy to provide any additional info you need!

Thank you!

I think I can't understand what kind of an example I need to provide.

I’d like to be able to paste your code into a command-line tool and run it. When I do that, it fails on line 1 which tries to import ASN1Decoder.

Oh, one other thing. Earlier I wrote:

I suspect your using the standard jwt.io test payload but it’d be good for you to include that here.

That comment was based on a misunderstanding of what you posted. Please ignore it.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

I see now. Here's the example:

import Foundation
import CryptoKit

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 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 = .eciesEncryptionStandardX963SHA384AESGCM

let result = SecKeyVerifySignature(publicKey,
                                   algorithm,
                                   dataSigned as NSData,
                                   dataSignature as NSData,
                                   &validateError)

if let validateError = validateError {
     print(validateError)
}

print("JWT is valid: \(result)") // prints JWT is valid: false

I can't realise what should be the SecKeyAlgorithm (if that's the problem at all).

Thank you for the assistance!

Hi, @escimo!

Were you able to try the example above? Thank you!

Bah, somehow I missed your earlier post. Sorry.

I plugged your code into a tiny project and the compiler complains about base64StringWithPadding(…). What does that look like?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Oh, sorry, it's a simple function to satisfy the base64 length requirements:

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))!

        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)
            
            &#x2F;&#x2F; we have the SecKey representation of the public key.
            &#x2F;&#x2F; validate the JWT with the public key.
            
            var validateError : Unmanaged&lt;CFError&gt;?
            let algorithm: SecKeyAlgorithm = .eciesEncryptionStandardX963SHA384AESGCM
            
            let result = SecKeyVerifySignature(publicKey,
                                               algorithm,
                                               dataSigned as NSData,
                                               dataSignature as NSData,
                                               &amp;validateError)
            
            if let validateError = validateError {
                print(validateError)
            }
            
            print("JWT is valid: \(result)") &#x2F;&#x2F; prints JWT is valid: false
        }
    }
    
    static func base64StringWithPadding(base64str: String) -&gt; String {
        var newStr = base64str.replacingOccurrences(of: "-", with: "+")
                    .replacingOccurrences(of: "_", with: "&#x2F;")
        let count = newStr.count % 4
        if count &gt; 0 {
          let amount = 4 - count
          for _ in 0..&lt;amount {
              newStr += "="
          }
        }
        return newStr
     }
}

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 around dataSignatureX962, 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)
            
            &#x2F;&#x2F; we have the SecKey representation of the public key.
            &#x2F;&#x2F; validate the JWT with the public key.
            
            var validateError : Unmanaged&lt;CFError&gt;?
            let algorithm: SecKeyAlgorithm = .ecdsaSignatureMessageX962SHA384
            
            let result = SecKeyVerifySignature(publicKey,
                                               algorithm,
                                               dataSigned as NSData,
                                               dataSignatureX962 as NSData,
                                               &amp;validateError)
            
            if let validateError = validateError {
                print(validateError)
            }
            
            print("JWT is valid: \(result)") &#x2F;&#x2F; prints JWT is valid: false
        }
    }
    
    static func base64StringWithPadding(base64str: String) -&gt; String {
        var newStr = base64str.replacingOccurrences(of: "-", with: "+")
                    .replacingOccurrences(of: "_", with: "&#x2F;")
        let count = newStr.count % 4
        if count &gt; 0 {
          let amount = 4 - count
          for _ in 0..&lt;amount {
              newStr += "="
          }
        }
        return newStr
     }
}

JWTValidator.validateSignature()

Hi again, thanks for the reply!

I have tried the .ecdsaSignatureMessageX962SHA384 algorithm before, but without converting the raw signature into X9.62 format.

This saves the day:

let dataSignatureX962 = try! P384.Signing.ECDSASignature(rawRepresentation: dataSignature).derRepresentation

It works now, thank you for your time and the support!