如何使用 Philips Hue Bridge 在 Android 应用程序上配置 SSL 证书以进行 API 调用?

问题描述 投票:0回答:1

首先声明,我对 SSL/TLS、证书、CA(证书颁发机构)基本上一无所知,而且我刚刚开始学习 Kotlin 和 Android 开发。我的主要目标是制作一个应用程序,其中包括通过互联网与飞利浦 Hue Bridge 进行通信。

通过向 Bridge 发送 API 调用来管理 Philips Hue 设备。 关灯的有效 PUT 请求示例:

https://192.168.100.8/clip/v2/resource/light/1c39aeb9-acee-4927-9450-bdb74e9ff5ba
,标头:
hue-application-key: gy5WelPhUBgxIR0HtZy-KVLU9QB7jaFeJIvE9WkZ
,正文:

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

作为起点,我尝试发送简单的 PUT 请求来打开和关闭灯泡。出于学习目的,整个应用程序仅由 1 个屏幕组成,其中有 2 个按钮(“打开”和“关闭”)。

出于测试目的,我尝试了来自 chatGPT 和 Web 的一堆代码,这些代码禁用了 SSL 验证,效果非常棒。

现在我想以适当的安全性进行 API 调用,而不禁用 SSL 验证。从 chatGPT 和 Web 复制一堆代码后,我最终得到了错误:

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: []

从 chatGPT 和 Web 复制代码后,我最终得到了这段代码,这导致了上面的错误(忽略直接将证书内容粘贴为明文,这仅用于测试和学习目的;))

  • 文件 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>
}
  • 文件 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()
}
  • 文件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")
        }

    }
}

代码中的 CA 证书来自 Philips Hue 开发者页面(包括登录信息,因此下面提供了图像):

Philips Hue documentation page No.1

Philips Hue documentation page No.2

Philips Hue documentation page No.3

我不知道这是否有帮助,但这也是命令的输出

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

我尝试插入来自 Philips Hue 开发人员网站的 CA 证书,以及从

openssl
命令获得的证书,两者都会导致与开头所述的相同错误。

android rest ssl-certificate retrofit2 philips-hue
1个回答
0
投票

我更多地使用 chatGPT,提供了 Philips Hue 文档,它给了我这个:

按照文章建议解决问题文章建议 解决这个问题的两种方法:

  • 使用网桥 ID 作为主机名并添加自定义解析器:

您可以将 HTTP 客户端配置为使用网桥 ID (ecb5fafffe831568) 作为主机名。然后,使用自定义解析器(在 某些网络库或系统配置),您可以解决 该标识符对应于网桥的实际 IP 地址。这种方法 允许在名称中使用的名称之间存在直接关联 连接和证书中的主题名称。

  • 将自定义主机名验证器注入 HTTPS 客户端:

或者,您可以将自定义逻辑注入到您的 HTTPS 客户端中 以非标准方式验证主机名,绕过标准 SSL/TLS 验证。在这种情况下,验证者可以接受 如果证书的使用者名称 (CN) 与网桥匹配,则连接 ID,无论使用的 IP 地址如何。然而,这不太安全,因为 它本质上绕过了部分 SSL/TLS 验证机制。 对于 Android 应用程序,尤其是使用 OkHttp 和 Retrofit 时,可以通过创建和使用自定义 HostnameVerifier 来实现第二种方法。 这样的 HostnameVerifier 可能看起来像这样:

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

这对我有用,但是正如 chatGPT 指出的那样,我认为它根本不安全。

© www.soinside.com 2019 - 2024. All rights reserved.