emqx / CocoaMQTT

MQTT 5.0 client library for iOS and macOS written in Swift
https://www.emqx.com/en
Other
1.58k stars 413 forks source link

TLS issue #448

Open philipparndt opened 2 years ago

philipparndt commented 2 years ago

Hi,

when using the MQTT protocol with TLS, there are some issues (with versions 2.x and 1.x. LetsEncrypt certificates generated with Traefik are denied with errSSLXCertChainInvalid = -9807, /* invalid certificate chain */

However, the same certificate works when connecting with WSS. I did some debugging, and think the problem is somewhere in the CocoaAsyncSocket which comes with a lot of deprecated API calls. Did you already do some experiments with Apples network framework to get rid of the deprecated API and maybe of this issue?

This issue was originally reported here: https://github.com/philipparndt/mqtt-analyzer/issues/69 see https://github.com/philipparndt/mqtt-analyzer/issues/69#issuecomment-1079704198 for a comparison between MQTTS and WSS (MQTTAnalyzer is using CocoaMQTT)

leeway1208 commented 2 years ago

Hi. How about trying these codes below. 😄

 mqtt!.enableSSL = true
 mqtt!.allowUntrustCACertificate = true
philipparndt commented 2 years ago

Hi @leeway1208

yes, I did this ;)

I tried to connect with the following settings (this is working with MQTT.js for example)

The connection fails in this test (with errSSLXCertChainInvalid = -9807, / invalid certificate chain /)

func settings(mqtt: CocoaMQTT) {
    mqtt.enableSSL = true
    mqtt.allowUntrustCACertificate = true
    mqtt.username = "admin"
    mqtt.password = "password"

    mqtt.keepAlive = 60
    mqtt.autoReconnect = false
}

func testConnect() {
    let mqtt = CocoaMQTT(clientID: "some-id",
                         host: host,
                         port: 8883)

    settings(mqtt: mqtt)

    let caller = Caller()
    mqtt.delegate = caller

    if !mqtt.connect() {
        XCTAssertTrue(false)
    }

    wait_for {
        caller.isConnected
    }
}

However, the same test with WebSockets is working:

func testConnectWSS() {
    let websocket = CocoaMQTTWebSocket(uri: "/")
    let mqtt = CocoaMQTT(clientID: "some-id",
                         host: host,
                         port: 443,
                         socket: websocket)

    settings(mqtt: mqtt)

    let caller = Caller()
    mqtt.delegate = caller

    if !mqtt.connect() {
        XCTAssertTrue(false)
    }

    wait_for {
        caller.isConnected
    }
}

SSL is handled completely different, the MQTT connection is using CocoaAsyncSocket and the WebSocket connection is using StarScream.

leeway1208 commented 2 years ago

😄 hi @philipparndt Can you give me your host? I will test the connection. I just tried the host (https://test.mosquitto.org/) which is ok. Thanks~

philipparndt commented 2 years ago

😄 the host is on my local network with docker. The DNS name resolves to a Traefik container which uses LetsEncrypt to create a certificate.

I'll look how I can host this temporary in the internet and come back to you.

philipparndt commented 2 years ago

Hi @leeway1208 I have a server for you 😄

Try to connect to (no auth):

mqtts://cocoamqtt.rnd7.de:8883 wss://cocoamqtt.rnd7.de:443

The websocket connection will work wit CocoaMQTT, the MQTT connection fails with errSSLXCertChainInvalid = -9807, /* invalid certificate chain */

When I do the same with another MQTT Client (MQTT.js) both connections will work. Example:

import * as mqtt from "mqtt"

const client = mqtt.connect("mqtts://cocoamqtt.rnd7.de:8883")

client.on("connect", () => {
    console.log("connected")
    client.subscribe("#", (err) => { console.log(err, "subscribed") })
    client.subscribe("$SYS/#", (err) => { console.log(err, "subscribed") })
})

client.on("message", (topic, message) => { console.log(topic, message.toString()) })
leeway1208 commented 2 years ago

@philipparndt Thanks~ I will test it some days. If I have some new ideas, I will share with you.

leeway1208 commented 2 years ago

@philipparndt I found that apple no longer supports TLS1.0 and TLS1.1. These versions have been deprecated on Apple platforms as of iOS 15, iPadOS 15, macOS 12, watchOS 8, and tvOS 15, and support will be removed in future releases. Do you use these versions? https://developer.apple.com/news/?id=bv8ur34d

philipparndt commented 2 years ago

Hi @leeway1208 thank your ideas. I did some changes in the configuration and checked the connection with Wireshark.

When the connection is done with the MQTT protocol, it uses the TLS1.2 Protocol with TLS version 1.2 from Server Hello message:

Transport Layer Security
    TLSv1.2 Record Layer: Handshake Protocol: Server Hello
        Content Type: Handshake (22)
        Version: TLS 1.2 (0x0303)
        Length: 55
        Handshake Protocol: Server Hello
    TLSv1.2 Record Layer: Handshake Protocol: Certificate
        Content Type: Handshake (22)
        Version: TLS 1.2 (0x0303)
        Length: 875
        Handshake Protocol: Certificate
    TLSv1.2 Record Layer: Handshake Protocol: Server Key Exchange
        Content Type: Handshake (22)
        Version: TLS 1.2 (0x0303)
        Length: 333
        Handshake Protocol: Server Key Exchange
            Handshake Type: Server Key Exchange (12)
            Length: 329
            EC Diffie-Hellman Server Params
                Curve Type: named_curve (0x03)
                Named Curve: secp256r1 (0x0017)
                Pubkey Length: 65
                Pubkey: …
                Signature Algorithm: rsa_pkcs1_sha256 (0x0401)
                    Signature Hash Algorithm Hash: SHA256 (4)
                    Signature Hash Algorithm Signature: RSA (1)
                Signature Length: 256
                Signature: …
    TLSv1.2 Record Layer: Handshake Protocol: Server Hello Done
        Content Type: Handshake (22)
        Version: TLS 1.2 (0x0303)
        Length: 4
        Handshake Protocol: Server Hello Done

after this, the client responds with Certificate unknown

Transport Layer Security
    TLSv1.2 Record Layer: Alert (Level: Fatal, Description: Certificate Unknown)
        Content Type: Alert (21)
        Version: TLS 1.2 (0x0303)
        Length: 2
        Alert Message
            Level: Fatal (2)
            Description: Certificate Unknown (46)
philipparndt commented 2 years ago

Another breadcrumb:

When I try to connect from the iPhone simulator using Apples Network Framework, the TLS handshake is successful. It seems that figuring out how to write a Network Framework Layer for CocoaMQTT is worth some time.

Example code

// Create an outbound connection

let connection = NWConnection(host: "cocoamqtt.rnd7.de", port: 8883, using: .tls)

connection.stateUpdateHandler = { (newState) in
    print(newState)
}
connection.start(queue: DispatchQueue.global(qos: .userInitiated))
sleep(10)
image image
philipparndt commented 2 years ago

Hi @leeway1208

okay I have a working demonstrator with Apple Network, that can connect to the server with the MQTT protocol 😄 There is still some work to do and as this protocol was introduced with iOS 12, I had to update the version number. See: https://github.com/philipparndt/CocoaMQTT/tree/apple-network

leeway1208 commented 2 years ago

@philipparndt I try to solve the connection problems with GCDAsyncSocket. But I haven't found a solution yet. The function - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag is not called. When I connect to host (cocoamqtt.rnd7.de).
I will keep trying

leeway1208 commented 2 years ago

@philipparndt I downloaded four mqtt apps in the App Store, and they can't connect to the host(mqtts://cocoamqtt.rnd7.de) either. So I discussed this with my colleague and he provided some reference documents for us to see if there are any configuration issues.

Enable SSL/TLS for EMQX MQTT broker (https://www.emqx.com/en/blog/emqx-server-ssl-tls-secure-connection-configuration-guide) Hands-on Demo: Set up TLS/SSL to Ensure MQTT Security (https://youtu.be/W7SedpZzQdo) https://www.youtube.com/watch?v=aFDDq9dLnEI (https://youtu.be/aFDDq9dLnEI)

Thanks

philipparndt commented 2 years ago

Hi @leeway1208, Thanks for your investigation!

What apps did you test?

I don't think that it is a configuration issue. Maybe the configuration is not supported by CocoaAsyncSocket. The configuration works like this:

This setup is pretty common when using Kubernetes (ingress controller). I think this will get used more and more and should be fixed/improved.

I have now exchanged CocoaAsyncSocket with the Apple network framework on my branch, and it works perfectly. I still have to do more tests, but the following will work:

I will continue developing this branch and remove CocoaAsyncSocket totally, as it has a lot of deprecated code (since iOS13). It should even be possible to handle WebSocket connections with this framework.

For your main branch I see the following options:

leeway1208 commented 2 years ago

@philipparndt Thanks for your help. I think you are right. It is more likely that CocoaAsyncSocket or some old libraries do not support this TLS configuration. I hope we can find some documentation to explain this issue and I am looking forward to receiving your pr. Then I will try to implement dynamic connections.

the testing app which I used below: EasyMQTT MQTTAnalyzer MQTTTool MQTT Spy

If you have some new ideas. We can discuss more. 😄😄😄😄😄😄

philipparndt commented 2 years ago

the testing app which I used below: EasyMQTT MQTTAnalyzer MQTTTool MQTT Spy

MQTTAnalyzer is my App and is using CocoaMQTT 😉 EasyMQTT is using CocoaMQTT as well 😉 MQTTTool is using Moscapsule (but was not updated for a long time)

I will come back to you when I have more. Currently, I implement connecting with client certificates.

leeway1208 commented 2 years ago

@philipparndt haha. I find a way to connect the host. I write the sslSettings like this:

mqtt5!.allowUntrustCACertificate = true 
mqtt5!.sslSettings = [kCFStreamSSLPeerName as String: "cocoamqtt.rnd7.de" as NSObject] 

🎆🎆🎆

philipparndt commented 2 years ago

@philipparndt haha. I find a way to connect the host. I write the sslSettings like this:

mqtt5!.allowUntrustCACertificate = true 
mqtt5!.sslSettings = [kCFStreamSSLPeerName as String: "cocoamqtt.rnd7.de" as NSObject] 

🎆🎆🎆

I can confirm this is working 👍 Nevertheless, I will continue the branch so that there is way less deprecated code 😉 I think we might need this for the next major iOS version anyway.

philipparndt commented 2 years ago

Hi @leeway1208,

I've implemented the changes on this branch: https://github.com/philipparndt/CocoaMQTT/pull/1 do you like to have a look at it?

While testing it I good some luck and seen some data races that are already reported by other users. I fixed them as well. Fixed:

leeway1208 commented 2 years ago

@philipparndt Well done! Thank you for sharing this which I can practice in my code as well. I will study your code and start preparing for dynamic connections. Hope you can provide feedback when I'm done. Thanks.

philipparndt commented 2 years ago

@leeway1208 let me know when can help you or you have something we should discuss. Maybe it would be a good idea to specify some small API for the connection provider so that we can split-up the work a little bit.

leeway1208 commented 2 years ago

@philipparndt Hi~~ Sorry, I can only start to work on this until May. But I can create a AppleNetwork branch for us first. haha