Skip to content

Commit

Permalink
Add support for certificates and keys in PEM string format in the MQT…
Browse files Browse the repository at this point in the history
…TClient TLSSettings
  • Loading branch information
davidepianca98 committed Feb 23, 2024
1 parent 02683d6 commit 5ee298c
Show file tree
Hide file tree
Showing 25 changed files with 1,081 additions and 142 deletions.
4 changes: 2 additions & 2 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,15 +109,15 @@ fun main() {
```

#### TLS code example
The certificates and key must be in PEM format, the password can be null
The certificates and key must be in PEM format, and they can be either a path to the PEM file or the actual PEM string. The password can be null if the private key has no protection.
```kotlin
fun main() {
val client = MQTTClient(
MQTTVersion.MQTT5,
"test.mosquitto.org",
8883,
TLSClientSettings(
serverCertificatePath = "mosquitto.org.crt",
serverCertificate = "mosquitto.org.crt",
)
) {
println(it.payload?.toByteArray()?.decodeToString())
Expand Down
4 changes: 2 additions & 2 deletions kmqtt-broker/src/commonTest/kotlin/integration/TLSTest.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package integration

import MQTTClient
import TLSClientSettings
import com.goncalossilva.resources.Resource
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
Expand All @@ -13,6 +12,7 @@ import mqtt.broker.Broker
import mqtt.packets.Qos
import mqtt.packets.mqttv5.ReasonCode
import mqtt.packets.mqttv5.SubscriptionOptions
import socket.tls.TLSClientSettings
import socket.tls.TLSSettings
import kotlin.test.Test
import kotlin.test.assertContentEquals
Expand All @@ -27,7 +27,7 @@ class TLSTest {
var received = false

val broker = Broker(port = 8883, tlsSettings = TLSSettings(keyStoreFilePath = pathPrefix + "keyStore.p12", keyStorePassword = "changeit"))
val client = MQTTClient(MQTTVersion.MQTT5, "127.0.0.1", broker.port, TLSClientSettings(serverCertificatePath = pathPrefix + "cert.pem")) {
val client = MQTTClient(MQTTVersion.MQTT5, "127.0.0.1", broker.port, TLSClientSettings(serverCertificate = pathPrefix + "cert.pem")) {
assertEquals(topic, it.topicName)
assertContentEquals(sendPayload.encodeToByteArray().toUByteArray(), it.payload)
assertEquals(Qos.AT_MOST_ONCE, it.qos)
Expand Down
1 change: 1 addition & 0 deletions kmqtt-client/src/commonMain/kotlin/MQTTClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import socket.IOException
import socket.SocketClosedException
import socket.SocketInterface
import socket.streams.EOFException
import socket.tls.TLSClientSettings

/**
* MQTT 3.1.1 and 5 client
Expand Down
1 change: 1 addition & 0 deletions kmqtt-client/src/commonMain/kotlin/TLSClientSocket.kt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import socket.tls.TLSClientSettings
import socket.tls.TLSSocket

public expect class TLSClientSocket(
Expand Down
76 changes: 68 additions & 8 deletions kmqtt-client/src/iosArm64Main/kotlin/TLSClientEngine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import platform.posix.getenv
import platform.posix.strcpy
import platform.posix.strlen
import socket.IOException
import socket.tls.TLSClientSettings
import socket.tls.TLSEngine

internal actual class TLSClientEngine actual constructor(tlsSettings: TLSClientSettings) : TLSEngine {
Expand All @@ -12,6 +13,42 @@ internal actual class TLSClientEngine actual constructor(tlsSettings: TLSClientS
private val readBio: CPointer<BIO>
private val writeBio: CPointer<BIO>

fun loadServerCertsFromString(context: CPointer<SSL_CTX>, certBuffer: String): Boolean {
val bio = BIO_new(BIO_s_mem())
BIO_puts(bio, certBuffer)

val inf = PEM_X509_INFO_read_bio(bio, null, null, null)

if (inf == null) {
BIO_free(bio)
return false
}

val ctx = SSL_CTX_get_cert_store(context);

var loaded = 0
for (i in 0..<sk_X509_INFO_num(inf)) {
val itmp = sk_X509_INFO_value(inf, i)
if (itmp != null && itmp.pointed.x509 != null) {
if (X509_STORE_add_cert(ctx, itmp.pointed.x509) != 1) {
sk_X509_INFO_pop_free(inf, staticCFunction { buf: CPointer<X509_INFO>? ->
X509_INFO_free(buf)
})
BIO_free(bio)
return false
}
loaded++
}
}

sk_X509_INFO_pop_free(inf, staticCFunction { buf: CPointer<X509_INFO>? ->
X509_INFO_free(buf)
})
BIO_free(bio)

return loaded > 0
}

init {
val readBio = BIO_new(BIO_s_mem()) ?: throw IOException("Failed allocating read BIO")

Expand All @@ -24,22 +61,45 @@ internal actual class TLSClientEngine actual constructor(tlsSettings: TLSClientS
val method = TLS_client_method()
val sslContext = SSL_CTX_new(method)!!

if (tlsSettings.serverCertificatePath != null) {
if (SSL_CTX_load_verify_locations(sslContext, tlsSettings.serverCertificatePath, null) != 1) {
throw Exception("Server certificate path not found")
if (tlsSettings.serverCertificate != null) {
if (!loadServerCertsFromString(sslContext, tlsSettings.serverCertificate!!)) {
// Try file
if (SSL_CTX_load_verify_locations(sslContext, tlsSettings.serverCertificate, null) != 1) {
throw Exception("Server certificate path not found")
}
}
} else {
if (SSL_CTX_load_verify_locations(sslContext, null, getenv(X509_get_default_cert_dir_env()?.toKString())?.toKString()) != 1) {
throw Exception("Server certificate path not found")
}
}

if (tlsSettings.clientCertificatePath != null) {
if (SSL_CTX_use_certificate_file(sslContext, tlsSettings.clientCertificatePath, SSL_FILETYPE_PEM) != 1) {
throw Exception("Cannot load client's certificate file")
if (tlsSettings.clientCertificate != null) {
val bio = BIO_new(BIO_s_mem())
BIO_puts(bio, tlsSettings.clientCertificate)
val certificate = PEM_read_bio_X509(bio, null, null, null)

if (certificate != null) {
if (SSL_CTX_use_certificate(sslContext, certificate) != 1) {
throw Exception("Cannot load client's certificate")
}
} else {
// Load file
if (SSL_CTX_use_certificate_file(sslContext, tlsSettings.clientCertificate, SSL_FILETYPE_PEM) != 1) {
throw Exception("Cannot load client's certificate file")
}
}
if (SSL_CTX_use_PrivateKey_file(sslContext, tlsSettings.clientCertificateKeyPath!!, SSL_FILETYPE_PEM) != 1) {
throw Exception("Cannot load client's key file")

BIO_puts(bio, tlsSettings.clientCertificateKey)
val key = PEM_read_bio_PrivateKey(bio, null, null, null)
if (key != null) {
if (SSL_CTX_use_PrivateKey(sslContext, key) != 1) {
throw Exception("Cannot load client's key")
}
} else {
if (SSL_CTX_use_PrivateKey_file(sslContext, tlsSettings.clientCertificateKey!!, SSL_FILETYPE_PEM) != 1) {
throw Exception("Cannot load client's key file")
}
}
if (SSL_CTX_check_private_key(sslContext) != 1) {
throw Exception("Client's certificate and key don't match")
Expand Down
76 changes: 68 additions & 8 deletions kmqtt-client/src/iosSimulatorArm64Main/kotlin/TLSClientEngine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import platform.posix.getenv
import platform.posix.strcpy
import platform.posix.strlen
import socket.IOException
import socket.tls.TLSClientSettings
import socket.tls.TLSEngine

internal actual class TLSClientEngine actual constructor(tlsSettings: TLSClientSettings) : TLSEngine {
Expand All @@ -12,6 +13,42 @@ internal actual class TLSClientEngine actual constructor(tlsSettings: TLSClientS
private val readBio: CPointer<BIO>
private val writeBio: CPointer<BIO>

fun loadServerCertsFromString(context: CPointer<SSL_CTX>, certBuffer: String): Boolean {
val bio = BIO_new(BIO_s_mem())
BIO_puts(bio, certBuffer)

val inf = PEM_X509_INFO_read_bio(bio, null, null, null)

if (inf == null) {
BIO_free(bio)
return false
}

val ctx = SSL_CTX_get_cert_store(context);

var loaded = 0
for (i in 0..<sk_X509_INFO_num(inf)) {
val itmp = sk_X509_INFO_value(inf, i)
if (itmp != null && itmp.pointed.x509 != null) {
if (X509_STORE_add_cert(ctx, itmp.pointed.x509) != 1) {
sk_X509_INFO_pop_free(inf, staticCFunction { buf: CPointer<X509_INFO>? ->
X509_INFO_free(buf)
})
BIO_free(bio)
return false
}
loaded++
}
}

sk_X509_INFO_pop_free(inf, staticCFunction { buf: CPointer<X509_INFO>? ->
X509_INFO_free(buf)
})
BIO_free(bio)

return loaded > 0
}

init {
val readBio = BIO_new(BIO_s_mem()) ?: throw IOException("Failed allocating read BIO")

Expand All @@ -24,22 +61,45 @@ internal actual class TLSClientEngine actual constructor(tlsSettings: TLSClientS
val method = TLS_client_method()
val sslContext = SSL_CTX_new(method)!!

if (tlsSettings.serverCertificatePath != null) {
if (SSL_CTX_load_verify_locations(sslContext, tlsSettings.serverCertificatePath, null) != 1) {
throw Exception("Server certificate path not found")
if (tlsSettings.serverCertificate != null) {
if (!loadServerCertsFromString(sslContext, tlsSettings.serverCertificate!!)) {
// Try file
if (SSL_CTX_load_verify_locations(sslContext, tlsSettings.serverCertificate, null) != 1) {
throw Exception("Server certificate path not found")
}
}
} else {
if (SSL_CTX_load_verify_locations(sslContext, null, getenv(X509_get_default_cert_dir_env()?.toKString())?.toKString()) != 1) {
throw Exception("Server certificate path not found")
}
}

if (tlsSettings.clientCertificatePath != null) {
if (SSL_CTX_use_certificate_file(sslContext, tlsSettings.clientCertificatePath, SSL_FILETYPE_PEM) != 1) {
throw Exception("Cannot load client's certificate file")
if (tlsSettings.clientCertificate != null) {
val bio = BIO_new(BIO_s_mem())
BIO_puts(bio, tlsSettings.clientCertificate)
val certificate = PEM_read_bio_X509(bio, null, null, null)

if (certificate != null) {
if (SSL_CTX_use_certificate(sslContext, certificate) != 1) {
throw Exception("Cannot load client's certificate")
}
} else {
// Load file
if (SSL_CTX_use_certificate_file(sslContext, tlsSettings.clientCertificate, SSL_FILETYPE_PEM) != 1) {
throw Exception("Cannot load client's certificate file")
}
}
if (SSL_CTX_use_PrivateKey_file(sslContext, tlsSettings.clientCertificateKeyPath!!, SSL_FILETYPE_PEM) != 1) {
throw Exception("Cannot load client's key file")

BIO_puts(bio, tlsSettings.clientCertificateKey)
val key = PEM_read_bio_PrivateKey(bio, null, null, null)
if (key != null) {
if (SSL_CTX_use_PrivateKey(sslContext, key) != 1) {
throw Exception("Cannot load client's key")
}
} else {
if (SSL_CTX_use_PrivateKey_file(sslContext, tlsSettings.clientCertificateKey!!, SSL_FILETYPE_PEM) != 1) {
throw Exception("Cannot load client's key file")
}
}
if (SSL_CTX_check_private_key(sslContext) != 1) {
throw Exception("Client's certificate and key don't match")
Expand Down
76 changes: 68 additions & 8 deletions kmqtt-client/src/iosX64Main/kotlin/TLSClientEngine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import platform.posix.getenv
import platform.posix.strcpy
import platform.posix.strlen
import socket.IOException
import socket.tls.TLSClientSettings
import socket.tls.TLSEngine

internal actual class TLSClientEngine actual constructor(tlsSettings: TLSClientSettings) : TLSEngine {
Expand All @@ -12,6 +13,42 @@ internal actual class TLSClientEngine actual constructor(tlsSettings: TLSClientS
private val readBio: CPointer<BIO>
private val writeBio: CPointer<BIO>

fun loadServerCertsFromString(context: CPointer<SSL_CTX>, certBuffer: String): Boolean {
val bio = BIO_new(BIO_s_mem())
BIO_puts(bio, certBuffer)

val inf = PEM_X509_INFO_read_bio(bio, null, null, null)

if (inf == null) {
BIO_free(bio)
return false
}

val ctx = SSL_CTX_get_cert_store(context);

var loaded = 0
for (i in 0..<sk_X509_INFO_num(inf)) {
val itmp = sk_X509_INFO_value(inf, i)
if (itmp != null && itmp.pointed.x509 != null) {
if (X509_STORE_add_cert(ctx, itmp.pointed.x509) != 1) {
sk_X509_INFO_pop_free(inf, staticCFunction { buf: CPointer<X509_INFO>? ->
X509_INFO_free(buf)
})
BIO_free(bio)
return false
}
loaded++
}
}

sk_X509_INFO_pop_free(inf, staticCFunction { buf: CPointer<X509_INFO>? ->
X509_INFO_free(buf)
})
BIO_free(bio)

return loaded > 0
}

init {
val readBio = BIO_new(BIO_s_mem()) ?: throw IOException("Failed allocating read BIO")

Expand All @@ -24,22 +61,45 @@ internal actual class TLSClientEngine actual constructor(tlsSettings: TLSClientS
val method = TLS_client_method()
val sslContext = SSL_CTX_new(method)!!

if (tlsSettings.serverCertificatePath != null) {
if (SSL_CTX_load_verify_locations(sslContext, tlsSettings.serverCertificatePath, null) != 1) {
throw Exception("Server certificate path not found")
if (tlsSettings.serverCertificate != null) {
if (!loadServerCertsFromString(sslContext, tlsSettings.serverCertificate!!)) {
// Try file
if (SSL_CTX_load_verify_locations(sslContext, tlsSettings.serverCertificate, null) != 1) {
throw Exception("Server certificate path not found")
}
}
} else {
if (SSL_CTX_load_verify_locations(sslContext, null, getenv(X509_get_default_cert_dir_env()?.toKString())?.toKString()) != 1) {
throw Exception("Server certificate path not found")
}
}

if (tlsSettings.clientCertificatePath != null) {
if (SSL_CTX_use_certificate_file(sslContext, tlsSettings.clientCertificatePath, SSL_FILETYPE_PEM) != 1) {
throw Exception("Cannot load client's certificate file")
if (tlsSettings.clientCertificate != null) {
val bio = BIO_new(BIO_s_mem())
BIO_puts(bio, tlsSettings.clientCertificate)
val certificate = PEM_read_bio_X509(bio, null, null, null)

if (certificate != null) {
if (SSL_CTX_use_certificate(sslContext, certificate) != 1) {
throw Exception("Cannot load client's certificate")
}
} else {
// Load file
if (SSL_CTX_use_certificate_file(sslContext, tlsSettings.clientCertificate, SSL_FILETYPE_PEM) != 1) {
throw Exception("Cannot load client's certificate file")
}
}
if (SSL_CTX_use_PrivateKey_file(sslContext, tlsSettings.clientCertificateKeyPath!!, SSL_FILETYPE_PEM) != 1) {
throw Exception("Cannot load client's key file")

BIO_puts(bio, tlsSettings.clientCertificateKey)
val key = PEM_read_bio_PrivateKey(bio, null, null, null)
if (key != null) {
if (SSL_CTX_use_PrivateKey(sslContext, key) != 1) {
throw Exception("Cannot load client's key")
}
} else {
if (SSL_CTX_use_PrivateKey_file(sslContext, tlsSettings.clientCertificateKey!!, SSL_FILETYPE_PEM) != 1) {
throw Exception("Cannot load client's key file")
}
}
if (SSL_CTX_check_private_key(sslContext) != 1) {
throw Exception("Client's certificate and key don't match")
Expand Down
Loading

0 comments on commit 5ee298c

Please sign in to comment.