agens-no / EllipticCurveKeyPair

Sign, verify, encrypt and decrypt using the Secure Enclave
Other
708 stars 114 forks source link

Signature validates locally but not on remote server #31

Open smamczak opened 6 years ago

smamczak commented 6 years ago

Hi, I wanto to use your library for my project which uses JSON Web Token JWT = base64(header) + "." + base64(payload) + "." + base64(signature)

In the documentation of my project I have an example

eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJ0ZXN0dXNlcm5hbWUiLCJzdWIiOiJ0ZXN0Y2xpZW50aWQiLCJpYXQiOjE1MDE1MDk3ODIsImV4cCI6MTUwMTUwOTg0Mn0.SGNpyl3zRA_ptRhA0lFH0o7-nhf3mpxE95ss37_jHYbCnwlRb4zDvVaYCj9DlpppU4U0y3vIPEqM44vV2UZ5Iw

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7u8bg5nOOsxZvkdmK+Zcvx+byi93
iQ+lMWHsAcOaOAwbmcSU3lKEXKu3gp/ymiXUhIyFuw2Pkxfe7T1e4HSmqA==
-----END PUBLIC KEY-----

And when I paste that token here https://jwt.io/ with the public key - it validates. However when I use your library to sign the header and payload I get info about invalid signature (on JWT page)

let signatureInput = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJ0ZXN0dXNlcm5hbWUiLCJzdWIiOiJ0ZXN0Y2xpZW50aWQiLCJpYXQiOjE1MDE1MDk3ODIsImV4cCI6MTUwMTUwOTg0Mn0"
let signature = createSignature(signatureInput)
let token = signatureInput + "." + signature

func createSignature(_ input: String) -> String {
        guard
            let data = input.data(using: .utf8),
            let signature = try? keysManager.sign(data, hash: .sha256)
        else {
            fatalError("Cannot create signature")
        }
        return signature.base64EncodedString(options: .init(rawValue: 0))
    }

The verification passes your method

do {
    try keysManager.verify(signature: Data(base64Encoded: signature)!,
         originalDigest: signatureInput.data(using: .utf8)!,
         hash: .sha256)
     print("valid")
}
catch {
     print("error \(error)")
}

Unbased content of header and payload are

header // eyJhbGciOiJFUzI1NiJ9
{
  "alg": "ES256"
}

payload // eyJpc3MiOiJ0ZXN0dXNlcm5hbWUiLCJzdWIiOiJ0ZXN0Y2xpZW50aWQiLCJpYXQiOjE1MDE1MDk3ODIsImV4cCI6MTUwMTUwOTg0Mn0
{
  "iss": "testusername",
  "sub": "testclientid",
  "iat": 1501509782,
  "exp": 1501509842
}

Am I doing something wrong with signing?

hfossli commented 6 years ago

Hi! Thanks for letting me know about this use case!

It was slightly tricky. 2 things where off.

  1. The signature that Security/EllipticCurveKeyPair produces is in DER format. The API expects a raw signature of 64 bytes for ES256.
  2. The API expects a base64 uri safe variant

I managed to produce a valid JWToken

eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJ0ZXN0dXNlcm5hbWUiLCJzdWIiOiJ0ZXN0Y2xpZW50aWQiLCJpYXQiOjE1MDE1MDk3ODIsImV4cCI6MTUwMTUwOTg0Mn0.9bUeggjl9sOdfxqOOcMv7uE6MUBrhEMWEFDLI1xiD4HwO8oSK_TuveST4imEug13h7hthgy1uwYPa0Fb7M4N3w

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEXum2ZYo0Qp2foJRIKPP2eGNT82y6
GgZgRGKWB8DArDhKQAhjp/RQFCoP8Olq4QtL5l4jcdKZhvOTAd47r7tAvQ==
-----END PUBLIC KEY-----

Here's the code

import EllipticCurveKeyPair

extension Data {
    func base64EncodedStringURISafe() -> String {
        return self.base64EncodedString()
            .replacingOccurrences(of: "+", with: "-")
            .replacingOccurrences(of: "/", with: "_")
            .replacingOccurrences(of: "=", with: "")
    }
}

func DEREC256SignatureDecode(_ signature: Data) -> Data {
    // example https://lapo.it/asn1js/#3046022100F309E36DFA5FE0BFBF5C3855E06E9C3C7D04DE347E2B345C3392DDB98E13BE6302210080372B3BBAE5E370B976092B8AA64F4EF1025FFE893D0046FA085F256AE04761
    var decoded = signature
    let maxChunkSize = 32
    decoded.removeFirst() // removing sequence header
    decoded.removeFirst() // removing sequence size
    decoded.removeFirst() // removing 'r' element header
    let rLength = Int(decoded.removeFirst()) // removing 'r' element length
    let r  = decoded.prefix(rLength).suffix(maxChunkSize) // read out 'r' bytes and discard any padding
    decoded.removeFirst(Int(rLength)) // removing 'r' bytes
    decoded.removeFirst() // 's' element header
    let sLength = Int(decoded.removeFirst()) // 's' element length
    let s  = decoded.prefix(sLength).suffix(maxChunkSize) // read out 's' bytes and discard any padding
    return Data(r) + Data(s)
}

let keysManager: EllipticCurveKeyPair.Manager = {
    let publicAccessControl = EllipticCurveKeyPair.AccessControl(protection: kSecAttrAccessibleAlwaysThisDeviceOnly, flags: [])
    let privateAccessControl = EllipticCurveKeyPair.AccessControl(protection: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, flags: [.privateKeyUsage, .userPresence])
    let config = EllipticCurveKeyPair.Config(
        publicLabel: "test.sign.public",
        privateLabel: "test.sign.private",
        operationPrompt: "Json web token",
        publicKeyAccessControl: publicAccessControl,
        privateKeyAccessControl: privateAccessControl,
        token: .secureEnclave)
    return EllipticCurveKeyPair.Manager(config: config)
}()

func testJWT() {
    do {
        try? keysManager.deleteKeyPair()

        let headerAndPayload = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJ0ZXN0dXNlcm5hbWUiLCJzdWIiOiJ0ZXN0Y2xpZW50aWQiLCJpYXQiOjE1MDE1MDk3ODIsImV4cCI6MTUwMTUwOTg0Mn0"
        let derSignature = try keysManager.sign(headerAndPayload.data(using: .utf8)!, hash: .sha256)
        let decodedSignature = DEREC256SignatureDecode(derSignature)
        let token = headerAndPayload + "." + decodedSignature.base64EncodedStringURISafe()

        print("headerAndPayloadAsString: \(headerAndPayload)")
        print("signature: \(derSignature.base64EncodedStringURISafe())")
        print("decoded signature: \(decodedSignature.base64EncodedStringURISafe())")
        print("token: \(token)")
        print("public key: \n\(try keysManager.publicKey().data().PEM)")

        try keysManager.verify(signature: derSignature,
                               originalDigest: headerAndPayload.data(using: .utf8)!,
                               hash: .sha256)
        print("valid!")
    } catch {
        print("error: \(error)")
    }
}

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // Insert code here to initialize your application

        testJWT()
    }
hfossli commented 6 years ago

There's some mutual interests here https://github.com/yourkarma/JWT/issues/116

smamczak commented 6 years ago

Hi, everything works! I'm really grateful for the speed of response and example!

hfossli commented 6 years ago

Great! I'm keeping this open until I have this included somehow

smamczak commented 6 years ago

That would be nice too. The sign method could have a param like so

enum SignatureFormat {
    case der
    case raw
}
public func sign(_ digest: Data, hash: Hash, format: SignatureFormat = .der, context: LAContext? = nil) throws -> Data {
}

but this is just a suggestion

hfossli commented 6 years ago

👍👍 Yep, either that or return an object with two variables.