How to configure SSL certificates on Android App with Philips Hue Bridge for API calls?

76 Views Asked by At

I'll start with the statements, that I don't know basically anything about SSL/TLS, certificates, CA(Certificate Autority), and I just started learning Kotlin and Android development. My main goal is to make an app, which will include communicating with the Philips Hue Bridge over the Internet.

Managing Philips Hue devices is done by sending API calls to the Bridge. Exmaple of valid PUT request to turn the light off: https://192.168.100.8/clip/v2/resource/light/1c39aeb9-acee-4927-9450-bdb74e9ff5ba, with the header: hue-application-key: gy5WelPhUBgxIR0HtZy-KVLU9QB7jaFeJIvE9WkZ with the body of:

{
    "on": {
        "on": false
    }
}

As a starting point, I tried to send simple PUT request to turn on and off the light bulb. For learning purposes the whole app consists of only 1 screen in which there are 2 buttons ("turn on", and "turn off").

For testing purposes, I tried bunch of code from chatGPT and the Web which disabled SSL verification, and that works awesome.

Now I want to make API calls with proper security, without disabling SSL verification. After copying bunch of code from chatGPT and Web, I end up with the error:

Exception: javax.net.ssl.SSLPeerUnverifiedException: Hostname 192.168.100.8 not verified:
    certificate: sha256/B+owkWAiYKkuAJ9dkuxu8IHl/mrQMHWyFfUoy+Nue3E=
    DN: CN=ecb5fafffe831568,O=Philips Hue,C=NL
    subjectAltNames: []

After copying code from chatGPT and Web I ended up with this code, which is causing the error above (ignore pasting certificate content directly as plaintext, that's only for testing and learning purposes ;) )

  • File PhilipsHueAPI.kt:
package com.example.firsttest

import com.example.firsttest.dto.light.put.LightPutResponse
import com.example.firsttest.dto.light.put.LightPut
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.Headers
import retrofit2.http.PUT

interface PhilipsHueAPI {
    @PUT("/clip/v2/resource/light/1c39aeb9-acee-4927-9450-bdb74e9ff5ba")
    @Headers("hue-application-key: gy5WelPhUBgxIR0HtZy-KVLU9QB7jaFeJIvE9WkZ")
    suspend fun changeLightState(@Body payload: LightPut): Response<LightPutResponse>
}
  • File RetrofitInstance.kt:
package com.example.firsttest

import okhttp3.Interceptor
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.io.ByteArrayInputStream
import java.security.KeyStore
import javax.net.ssl.SSLContext
import java.security.cert.CertificateFactory
import javax.net.ssl.TrustManagerFactory


object RetrofitInstance {
    val api: PhilipsHueAPI by lazy {
        Retrofit.Builder()
            .baseUrl("https://192.168.100.8")
            .client(getOkHttpClient())
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(PhilipsHueAPI::class.java)
    }
}

fun getOkHttpClient(): OkHttpClient {
    val caCertificate = """-----BEGIN CERTIFICATE-----
MIICMjCCAdigAwIBAgIUO7FSLbaxikuXAljzVaurLXWmFw4wCgYIKoZIzj0EAwIw
OTELMAkGA1UEBhMCTkwxFDASBgNVBAoMC1BoaWxpcHMgSHVlMRQwEgYDVQQDDAty
b290LWJyaWRnZTAiGA8yMDE3MDEwMTAwMDAwMFoYDzIwMzgwMTE5MDMxNDA3WjA5
MQswCQYDVQQGEwJOTDEUMBIGA1UECgwLUGhpbGlwcyBIdWUxFDASBgNVBAMMC3Jv
b3QtYnJpZGdlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEjNw2tx2AplOf9x86
aTdvEcL1FU65QDxziKvBpW9XXSIcibAeQiKxegpq8Exbr9v6LBnYbna2VcaK0G22
jOKkTqOBuTCBtjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNV
HQ4EFgQUZ2ONTFrDT6o8ItRnKfqWKnHFGmQwdAYDVR0jBG0wa4AUZ2ONTFrDT6o8
ItRnKfqWKnHFGmShPaQ7MDkxCzAJBgNVBAYTAk5MMRQwEgYDVQQKDAtQaGlsaXBz
IEh1ZTEUMBIGA1UEAwwLcm9vdC1icmlkZ2WCFDuxUi22sYpLlwJY81Wrqy11phcO
MAoGCCqGSM49BAMCA0gAMEUCIEBYYEOsa07TH7E5MJnGw557lVkORgit2Rm1h3B2
sFgDAiEA1Fj/C3AN5psFMjo0//mrQebo0eKd3aWRx+pQY08mk48=
-----END CERTIFICATE-----
""".trimMargin()

    val certificateFactory = CertificateFactory.getInstance("X.509")

    val caInput = ByteArrayInputStream(caCertificate.toByteArray(Charsets.UTF_8))
    val ca = certificateFactory.generateCertificate(caInput)
    caInput.close()

    val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()).apply {
        load(null, null)
        setCertificateEntry("ca", ca)
    }

    val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).apply {
        init(keyStore)
    }

    val sslContext = SSLContext.getInstance("TLS").apply {
        init(null, tmf.trustManagers, null)
    }

    val rewriteHostInterceptor = Interceptor { chain ->
        val originalRequest = chain.request()
        val newRequest = originalRequest.newBuilder()
            .header("Host", "ecb5fafffe831568") // CN z Twojego certyfikatu
            .build()
        chain.proceed(newRequest)
    }

    return OkHttpClient.Builder()
        .addInterceptor(rewriteHostInterceptor)
        .sslSocketFactory(
            sslContext.socketFactory,
            tmf.trustManagers[0] as javax.net.ssl.X509TrustManager
        )
        .build()
}
  • File MainActivity.kt:
package com.example.firsttest

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import com.example.firsttest.databinding.WelcomeLayoutBinding
import com.example.firsttest.dto.light.put.LightPut
import com.example.firsttest.dto.light.put.On
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {

    private lateinit var binding: WelcomeLayoutBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = WelcomeLayoutBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.btnTurnOn.setOnClickListener {
            CoroutineScope(Dispatchers.IO).launch {
                changeLightState(true)
            }
        }

        binding.btnTurnOff.setOnClickListener {
            CoroutineScope(Dispatchers.IO).launch {
                changeLightState(false)
            }
        }
    }


    private suspend fun changeLightState(on: Boolean) {
        try {
            val response = RetrofitInstance.api.changeLightState(LightPut(On(on)))
            if (response.isSuccessful) {
                Log.d("MainActivity", "PUT RESPONSE: ${response.body().toString()}")
            } else {
                Log.d("MainActivity", "HTTP failed, response code: ${response.code()}")
            }
        } catch (e: Exception) {
            Log.e("MainActivity", "Exception: $e")
        }

    }
}

The CA certificate inside the code is from the Philips Hue Developers page (login included, that's why images are provided below):

Philips Hue documentation page No.1

Philips Hue documentation page No.2

Philips Hue documentation page No.3

I don't know if that could help, but here's also the output of the command openssl s_client -showcerts -connect 192.168.100.8:443:

Connecting to 192.168.100.8
CONNECTED(00000003)
Can't use SSL_get_servername
depth=1 C=NL, O=Philips Hue, CN=root-bridge
verify return:1
depth=0 C=NL, O=Philips Hue, CN=ecb5fafffe831568
verify return:1
---
Certificate chain
 0 s:C=NL, O=Philips Hue, CN=ecb5fafffe831568
   i:C=NL, O=Philips Hue, CN=root-bridge
   a:PKEY: id-ecPublicKey, 256 (bit); sigalg: ecdsa-with-SHA256
   v:NotBefore: Jan  1 00:00:00 2017 GMT; NotAfter: Jan 19 03:14:07 2038 GMT
-----BEGIN CERTIFICATE-----
MIICPTCCAeSgAwIBAgIJAOy1+v/+gxVoMAoGCCqGSM49BAMCMDkxCzAJBgNVBAYT
Ak5MMRQwEgYDVQQKDAtQaGlsaXBzIEh1ZTEUMBIGA1UEAwwLcm9vdC1icmlkZ2Uw
IhgPMjAxNzAxMDEwMDAwMDBaGA8yMDM4MDExOTAzMTQwN1owPjELMAkGA1UEBhMC
TkwxFDASBgNVBAoMC1BoaWxpcHMgSHVlMRkwFwYDVQQDDBBlY2I1ZmFmZmZlODMx
NTY4MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfyqVidfOTG8DPZuzuqqYMfv2
hl0i9g53pPpTpgDkv+AXN8wzddjysYsIiqBftZ6L4aZBOtjDM5+a+s89U5TRNqOB
yzCByDAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggr
BgEFBQcDATAdBgNVHQ4EFgQUp6R5UYWmhSDFBKXSk8l/e0yk8d8wdAYDVR0jBG0w
a4AUZ2ONTFrDT6o8ItRnKfqWKnHFGmShPaQ7MDkxCzAJBgNVBAYTAk5MMRQwEgYD
VQQKDAtQaGlsaXBzIEh1ZTEUMBIGA1UEAwwLcm9vdC1icmlkZ2WCFDuxUi22sYpL
lwJY81Wrqy11phcOMAoGCCqGSM49BAMCA0cAMEQCIHXRIFqBH6x0lfjCoSRzPszO
BwoKmGbte70cNYPrQSZsAiAOTMGMK8tfhBFTEQitcCFSVRl4S7k8/GPw5XO7675D
CQ==
-----END CERTIFICATE-----
---
Server certificate
subject=C=NL, O=Philips Hue, CN=ecb5fafffe831568
issuer=C=NL, O=Philips Hue, CN=root-bridge
---
No client certificate CA names sent
Peer signing digest: SHA256
Peer signature type: ECDSA
Server Temp Key: ECDH, prime256v1, 256 bits
---
SSL handshake has read 1066 bytes and written 425 bytes
Verification: OK
---
New, TLSv1.2, Cipher is ECDHE-ECDSA-AES128-GCM-SHA256
Server public key is 256 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : ECDHE-ECDSA-AES128-GCM-SHA256
    Session-ID: 635751762F2DB154B19BF117503766053887F4F1D475961EC71575EEB53B83C8
    Session-ID-ctx: 
    Master-Key: 95ADE1B8AFF8F79D55211103E2BE6EADECEB0B42BFAEC299881B9D2374946B17A8D893815459F1205365800E0E6A5021
    PSK identity: None
    PSK identity hint: None
    SRP username: None
    TLS session ticket lifetime hint: 86400 (seconds)
    TLS session ticket:
    0000 - 51 99 be 21 9e a5 bb 09-e5 4b 7b de 88 ae bc 12   Q..!.....K{.....
    0010 - a5 c9 62 03 44 a9 2a 82-d0 b2 1b b8 8d c6 90 e8   ..b.D.*.........
    0020 - 78 39 8c b2 43 29 40 ed-25 31 24 45 de aa c2 c3   x9..C)@.%1$E....
    0030 - dd 10 ac 5d d9 28 56 1e-ab df 8d 22 84 23 7d 7c   ...].(V....".#}|
    0040 - f0 cc 9b 9a 08 60 e9 8a-46 d3 04 33 13 9b 7f d3   .....`..F..3....
    0050 - df e7 ca 09 87 86 3d ee-d0 84 19 49 7f b5 88 c1   ......=....I....
    0060 - 32 66 e3 a6 0d 04 18 ac-2c f5 40 75 eb d8 ec c3   2f......,.@u....
    0070 - a4 c9 94 6b f1 78 fe ce-2b b6 73 7e 30 48 b4 22   ...k.x..+.s~0H."
    0080 - d1 87 1d 40 b0 a9 9d 3c-b9 e8 89 41 6c 6e 41 00   ...@...<...AlnA.
    0090 - 24 0a fd 8d 26 dd 1f c8-d8 da 93 97 33 09 6e 91   $...&.......3.n.
    00a0 - e7 de 89 64 df 20 c4 5d-63 9e f8 6f d7 e3 42 66   ...d. .]c..o..Bf

    Start Time: 1707495763
    Timeout   : 7200 (sec)
    Verify return code: 0 (ok)
    Extended master secret: yes
---
closed

I tried inserting the CA Certificate from the Philips Hue Developers website, aswell as the certificate I got from openssl command, both resulting in the same error descriped at the beginning.

1

There are 1 best solutions below

0
Klusio19 On

I played with chatGPT even more, provided the Philips Hue documentation and it gave me this:

Solving the Problem as Suggested by the Article The article suggests two approaches to solve this problem:

  • Using the bridge ID as the hostname and adding a custom resolver:

You can configure your HTTP client to use the bridge ID (ecb5fafffe831568) as the hostname. Then, with a custom resolver (in certain network libraries or system configurations), you can resolve this identifier to the actual IP address of the bridge. This approach allows for a direct correlation between the name used in the connection and the subject name in the certificate.

  • Injecting a custom hostname verifier into the HTTPS client:

Alternatively, you can inject custom logic into your HTTPS client that verifies the hostname in a non-standard way, bypassing the standard SSL/TLS verification. In this case, the verifier could accept the connection if the certificate's subject name (CN) matches the bridge ID, regardless of the used IP address. However, this is less secure as it essentially bypasses part of the SSL/TLS verification mechanisms. For an Android app, especially when using OkHttp and Retrofit, the second approach can be implemented by creating and using a custom HostnameVerifier. Such a HostnameVerifier might look something like this:

.hostnameVerifier { hostname, session ->
    hostname.equals("192.168.100.8", ignoreCase = true) ||
session.peerPrincipal.name.contains("CN=ecb5fafffe831568")
}

That worked for me, however as chatGPT pointed out, I don't think it is secure at all.