From 4d163e13bb52ed63e5dec92ab194736bad261051 Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Sun, 28 Sep 2025 17:57:20 +0100 Subject: [PATCH 1/7] add support for bridge v3 certificate Signed-off-by: Andrew Fiddian-Green --- .../connection/HueTlsTrustManagerProvider.java | 7 +++++-- .../hue/internal/handler/Clip2BridgeHandler.java | 3 ++- .../hue/internal/handler/HueBridgeHandler.java | 2 +- .../src/main/resources/huebridge3_cacert.pem | 15 +++++++++++++++ 4 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 bundles/org.openhab.binding.hue/src/main/resources/huebridge3_cacert.pem diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/HueTlsTrustManagerProvider.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/HueTlsTrustManagerProvider.java index d34657c5484bd..80420df5f2535 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/HueTlsTrustManagerProvider.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/HueTlsTrustManagerProvider.java @@ -39,16 +39,19 @@ public class HueTlsTrustManagerProvider implements TlsTrustManagerProvider { private static final String PEM_FILENAME = "huebridge_cacert.pem"; + private static final String PEM3_FILENAME = "huebridge3_cacert.pem"; private final String hostname; private final boolean useSelfSignedCertificate; + private final boolean isBridgeV3; private final Logger logger = LoggerFactory.getLogger(HueTlsTrustManagerProvider.class); private @Nullable PEMTrustManager trustManager; - public HueTlsTrustManagerProvider(String hostname, boolean useSelfSignedCertificate) { + public HueTlsTrustManagerProvider(String hostname, boolean useSelfSignedCertificate, boolean isBridgeV3) { this.hostname = hostname; this.useSelfSignedCertificate = useSelfSignedCertificate; + this.isBridgeV3 = isBridgeV3; } @Override @@ -78,7 +81,7 @@ public X509ExtendedTrustManager getTrustManager() { } else { logger.trace("Use Signify private CA Certificate for Hue Bridges from resources."); // use Signify private CA Certificate for Hue Bridges from resources - localTrustManager = getInstanceFromResource(PEM_FILENAME); + localTrustManager = getInstanceFromResource(isBridgeV3 ? PEM3_FILENAME : PEM_FILENAME); } this.trustManager = localTrustManager; } catch (CertificateException | MalformedURLException e) { diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java index e8a7ccb51b799..abacca1bd0f2a 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java @@ -494,8 +494,9 @@ private void initializeAssets() { return; } + boolean isBridgeV3 = "BSB003".equals(thing.getProperties().get(Thing.PROPERTY_MODEL_ID)); HueTlsTrustManagerProvider trustManagerProvider = new HueTlsTrustManagerProvider(ipAddress + ":443", - config.useSelfSignedCertificate); + config.useSelfSignedCertificate, isBridgeV3); if (Objects.isNull(trustManagerProvider.getPEMTrustManager())) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java index 64f658fefea00..2f05ea889d845 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java @@ -698,7 +698,7 @@ public void initialize() { scheduler.submit(() -> { // register trustmanager service HueTlsTrustManagerProvider tlsTrustManagerProvider = new HueTlsTrustManagerProvider( - ip + ":" + hueBridgeConfig.getPort(), hueBridgeConfig.useSelfSignedCertificate); + ip + ":" + hueBridgeConfig.getPort(), hueBridgeConfig.useSelfSignedCertificate, false); // Check before registering that the PEM certificate can be downloaded if (tlsTrustManagerProvider.getPEMTrustManager() == null) { diff --git a/bundles/org.openhab.binding.hue/src/main/resources/huebridge3_cacert.pem b/bundles/org.openhab.binding.hue/src/main/resources/huebridge3_cacert.pem new file mode 100644 index 0000000000000..c9d310863f73e --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/resources/huebridge3_cacert.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICUDCCAfWgAwIBAgIJAMQplv/+xOLYMAoGCCqGSM49BAMCMDkxCzAJBgNVBAYT +Ak5MMRQwEgYDVQQKDAtQaGlsaXBzIEh1ZTEUMBIGA1UEAwwLcm9vdC1icmlkZ2Uw +IhgPMjAyNTAyMjcxMzIxMTFaGA8yMDM4MDExOTAzMTQwN1owTzELMAkGA1UEBhMC +TkwxFDASBgNVBAoMC1BoaWxpcHMgSHVlMRkwFwYDVQQDDBBDNDI5OTZGRkZFQzRF +MkQ4MQ8wDQYDVQQLDAZCU0IwMDMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATr +eQ8P8I2okD2ypnXLNGniE14QoxYM1n7A/Ld3/G/VHbxAcOsRK+9fok4FsJ4jGJ8R +w9w8iHKtmySjk8frVBxvo4HLMIHIMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQD +AgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB0GA1UdDgQWBBTHTFUlMVjBfCRDW5Jn +rT1qvxaQhjB0BgNVHSMEbTBrgBRnY41MWsNPqjwi1Gcp+pYqccUaZKE9pDswOTEL +MAkGA1UEBhMCTkwxFDASBgNVBAoMC1BoaWxpcHMgSHVlMRQwEgYDVQQDDAtyb290 +LWJyaWRnZYIUO7FSLbaxikuXAljzVaurLXWmFw4wCgYIKoZIzj0EAwIDSQAwRgIh +AP31tUs6kG4a9CifLyi7MaFYZBcxMZY0u+yNFK2eCqXzAiEAnD9leje6HlDcgWft +316G3aFaj+wrBf6TzOwxNkPY0rg= +-----END CERTIFICATE----- From 8b469f6968313c4ce288df9290ca5260fedf5585 Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Sun, 28 Sep 2025 18:09:02 +0100 Subject: [PATCH 2/7] copilot fix Signed-off-by: Andrew Fiddian-Green --- .../binding/hue/internal/handler/Clip2BridgeHandler.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java index abacca1bd0f2a..bad5474308d56 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java @@ -86,6 +86,8 @@ @NonNullByDefault public class Clip2BridgeHandler extends BaseBridgeHandler { + private static final String BRIDGE_V3_MODEL_ID = "BSB003"; // model name of bridge v3 (black bridge) + public static final Set SUPPORTED_THING_TYPES = Set.of(THING_TYPE_BRIDGE_API2); private static final int FAST_SCHEDULE_MILLI_SECONDS = 500; @@ -494,7 +496,7 @@ private void initializeAssets() { return; } - boolean isBridgeV3 = "BSB003".equals(thing.getProperties().get(Thing.PROPERTY_MODEL_ID)); + boolean isBridgeV3 = BRIDGE_V3_MODEL_ID.equals(thing.getProperties().get(Thing.PROPERTY_MODEL_ID)); HueTlsTrustManagerProvider trustManagerProvider = new HueTlsTrustManagerProvider(ipAddress + ":443", config.useSelfSignedCertificate, isBridgeV3); From c386d3fbc69659c90cc3375c3e6175443823bacb Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Fri, 3 Oct 2025 17:08:52 +0100 Subject: [PATCH 3/7] fix discovery Signed-off-by: Andrew Fiddian-Green --- .../HueBridgeMDNSDiscoveryParticipant.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeMDNSDiscoveryParticipant.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeMDNSDiscoveryParticipant.java index e22689b7c3920..e7fed97af21fe 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeMDNSDiscoveryParticipant.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeMDNSDiscoveryParticipant.java @@ -19,6 +19,8 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.jmdns.ServiceInfo; @@ -57,6 +59,7 @@ public class HueBridgeMDNSDiscoveryParticipant implements MDNSDiscoveryParticipa private static final String SERVICE_TYPE = "_hue._tcp.local."; private static final String MDNS_PROPERTY_BRIDGE_ID = "bridgeid"; private static final String MDNS_PROPERTY_MODEL_ID = "modelid"; + private static final Pattern BSB_MODEL_ID_PATTERN = Pattern.compile("^BSB(\\d{3})$"); private final Logger logger = LoggerFactory.getLogger(HueBridgeMDNSDiscoveryParticipant.class); protected final ThingRegistry thingRegistry; @@ -109,6 +112,7 @@ public String getServiceType() { @Override public @Nullable DiscoveryResult createResult(ServiceInfo service) { + logger.debug("Discovered mDNS service: {}", service.getNiceTextString()); if (isAutoDiscoveryEnabled) { ThingUID uid = getThingUID(service); if (Objects.nonNull(uid)) { @@ -160,6 +164,9 @@ private Optional getLegacyBridge(String ipAddress) { String id = service.getPropertyString(MDNS_PROPERTY_BRIDGE_ID); if (id != null && !id.isBlank()) { id = id.toLowerCase(); + if (isOrAboveBSB003(service.getPropertyString(MDNS_PROPERTY_MODEL_ID))) { + return new ThingUID(THING_TYPE_BRIDGE_API2, id); + } try { return Clip2Bridge.isClip2Supported(service.getHostAddresses()[0]) ? new ThingUID(THING_TYPE_BRIDGE_API2, id) @@ -171,6 +178,15 @@ private Optional getLegacyBridge(String ipAddress) { return null; } + private boolean isOrAboveBSB003(@Nullable String modelId) { + Matcher matcher = BSB_MODEL_ID_PATTERN.matcher(modelId); + if (!matcher.matches()) { + return false; + } + int version = Integer.parseInt(matcher.group(1)); + return version >= 3; + } + @Override public long getRemovalGracePeriodSeconds(ServiceInfo service) { return removalGracePeriod; From 6da5cbc1971106307fdd9e74692d70fb38a1754c Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Fri, 3 Oct 2025 17:09:15 +0100 Subject: [PATCH 4/7] use other jpalo certificate Signed-off-by: Andrew Fiddian-Green --- .../src/main/resources/huebridge3_cacert.pem | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/bundles/org.openhab.binding.hue/src/main/resources/huebridge3_cacert.pem b/bundles/org.openhab.binding.hue/src/main/resources/huebridge3_cacert.pem index c9d310863f73e..9766019ff495b 100644 --- a/bundles/org.openhab.binding.hue/src/main/resources/huebridge3_cacert.pem +++ b/bundles/org.openhab.binding.hue/src/main/resources/huebridge3_cacert.pem @@ -1,15 +1,26 @@ ------BEGIN CERTIFICATE----- -MIICUDCCAfWgAwIBAgIJAMQplv/+xOLYMAoGCCqGSM49BAMCMDkxCzAJBgNVBAYT -Ak5MMRQwEgYDVQQKDAtQaGlsaXBzIEh1ZTEUMBIGA1UEAwwLcm9vdC1icmlkZ2Uw -IhgPMjAyNTAyMjcxMzIxMTFaGA8yMDM4MDExOTAzMTQwN1owTzELMAkGA1UEBhMC -TkwxFDASBgNVBAoMC1BoaWxpcHMgSHVlMRkwFwYDVQQDDBBDNDI5OTZGRkZFQzRF -MkQ4MQ8wDQYDVQQLDAZCU0IwMDMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATr -eQ8P8I2okD2ypnXLNGniE14QoxYM1n7A/Ld3/G/VHbxAcOsRK+9fok4FsJ4jGJ8R -w9w8iHKtmySjk8frVBxvo4HLMIHIMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQD -AgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB0GA1UdDgQWBBTHTFUlMVjBfCRDW5Jn -rT1qvxaQhjB0BgNVHSMEbTBrgBRnY41MWsNPqjwi1Gcp+pYqccUaZKE9pDswOTEL -MAkGA1UEBhMCTkwxFDASBgNVBAoMC1BoaWxpcHMgSHVlMRQwEgYDVQQDDAtyb290 -LWJyaWRnZYIUO7FSLbaxikuXAljzVaurLXWmFw4wCgYIKoZIzj0EAwIDSQAwRgIh -AP31tUs6kG4a9CifLyi7MaFYZBcxMZY0u+yNFK2eCqXzAiEAnD9leje6HlDcgWft -316G3aFaj+wrBf6TzOwxNkPY0rg= ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICMjCCAdigAwIBAgIUO7FSLbaxikuXAljzVaurLXWmFw4wCgYIKoZIzj0EAwIw +OTELMAkGA1UEBhMCTkwxFDASBgNVBAoMC1BoaWxpcHMgSHVlMRQwEgYDVQQDDAty +b290LWJyaWRnZTAiGA8yMDE3MDEwMTAwMDAwMFoYDzIwMzgwMTE5MDMxNDA3WjA5 +MQswCQYDVQQGEwJOTDEUMBIGA1UECgwLUGhpbGlwcyBIdWUxFDASBgNVBAMMC3Jv +b3QtYnJpZGdlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEjNw2tx2AplOf9x86 +aTdvEcL1FU65QDxziKvBpW9XXSIcibAeQiKxegpq8Exbr9v6LBnYbna2VcaK0G22 +jOKkTqOBuTCBtjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNV +HQ4EFgQUZ2ONTFrDT6o8ItRnKfqWKnHFGmQwdAYDVR0jBG0wa4AUZ2ONTFrDT6o8 +ItRnKfqWKnHFGmShPaQ7MDkxCzAJBgNVBAYTAk5MMRQwEgYDVQQKDAtQaGlsaXBz +IEh1ZTEUMBIGA1UEAwwLcm9vdC1icmlkZ2WCFDuxUi22sYpLlwJY81Wrqy11phcO +MAoGCCqGSM49BAMCA0gAMEUCIEBYYEOsa07TH7E5MJnGw557lVkORgit2Rm1h3B2 +sFgDAiEA1Fj/C3AN5psFMjo0//mrQebo0eKd3aWRx+pQY08mk48= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBzDCCAXOgAwIBAgICEAAwCgYIKoZIzj0EAwIwPDELMAkGA1UEBhMCTkwxFDAS +BgNVBAoMC1NpZ25pZnkgSHVlMRcwFQYDVQQDDA5IdWUgUm9vdCBDQSAwMTAgFw0y +NTAyMjUwMDAwMDBaGA8yMDUwMTIzMTIzNTk1OVowPDELMAkGA1UEBhMCTkwxFDAS +BgNVBAoMC1NpZ25pZnkgSHVlMRcwFQYDVQQDDA5IdWUgUm9vdCBDQSAwMTBZMBMG +ByqGSM49AgEGCCqGSM49AwEHA0IABFfOO0jfSAUXGQ9kjEDzyBrcMQ3ItyA5krE+ +cyvb1Y3xFti7KlAad8UOnAx0FBLn7HZrlmIwm1QnX0fK3LPM13mjYzBhMB0GA1Ud +DgQWBBTF1pSpsCASX/z0VHLigxU2CAaqoTAfBgNVHSMEGDAWgBTF1pSpsCASX/z0 +VHLigxU2CAaqoTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAKBggq +hkjOPQQDAgNHADBEAiAk7duT+IHbOGO4UUuGLAEpyYejGZK9Z7V9oSfnvuQ5BQIg +IYSgwwxHXm73/JgcU9lAM6c8Bmu3UE3kBIUwBs1qXFw= +-----END CERTIFICATE----- \ No newline at end of file From 14d298866ad1c67835a81662cf310273a166f97a Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Fri, 3 Oct 2025 21:47:09 +0100 Subject: [PATCH 5/7] work in progress Signed-off-by: Andrew Fiddian-Green --- .../HueTlsTrustManagerProvider.java | 27 ++++++++++++---- .../HueBridgeMDNSDiscoveryParticipant.java | 32 ++++++++++++------- .../internal/handler/Clip2BridgeHandler.java | 8 ++--- .../src/main/resources/huebridge3_cacert.pem | 26 --------------- .../main/resources/huebridge_cacert_v2.pem | 12 +++++++ 5 files changed, 57 insertions(+), 48 deletions(-) delete mode 100644 bundles/org.openhab.binding.hue/src/main/resources/huebridge3_cacert.pem create mode 100644 bundles/org.openhab.binding.hue/src/main/resources/huebridge_cacert_v2.pem diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/HueTlsTrustManagerProvider.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/HueTlsTrustManagerProvider.java index 80420df5f2535..1785b827b3ff5 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/HueTlsTrustManagerProvider.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/HueTlsTrustManagerProvider.java @@ -38,20 +38,32 @@ @NonNullByDefault public class HueTlsTrustManagerProvider implements TlsTrustManagerProvider { - private static final String PEM_FILENAME = "huebridge_cacert.pem"; - private static final String PEM3_FILENAME = "huebridge3_cacert.pem"; + private static final String PEM_CACERT_V1_FILENAME = "huebridge_cacert.pem"; + private static final String PEM_CACERT_V2_FILENAME = "huebridge_cacert_v2.pem"; private final String hostname; private final boolean useSelfSignedCertificate; - private final boolean isBridgeV3; + private final boolean useSignifyCaCertificateV2; private final Logger logger = LoggerFactory.getLogger(HueTlsTrustManagerProvider.class); private @Nullable PEMTrustManager trustManager; - public HueTlsTrustManagerProvider(String hostname, boolean useSelfSignedCertificate, boolean isBridgeV3) { + /** + * Creates a new instance of {@link HueTlsTrustManagerProvider}. + * See {@link https://developers.meethue.com/develop/application-design-guidance/using-https/} for more + * details about 'Signify private CA Certificates V1 and V2 for Hue Bridges'. + * + * @param hostname the hostname of the Hue Bridge + * @param useSelfSignedCertificate true, to use the self-signed certificate downloaded from the Hue Bridge; + * false, to use the Signify private CA Certificate V1 or V2 for Hue Bridges from resources + * @param useSignifyCaCerteficateV2 true, to use the 'Signify private CA Certificate V2 for Hue Bridges'; + * false, to use the 'Signify private CA Certificate V1 for Hue Bridges' + */ + public HueTlsTrustManagerProvider(String hostname, boolean useSelfSignedCertificate, + boolean useSignifyCaCerteficateV2) { this.hostname = hostname; this.useSelfSignedCertificate = useSelfSignedCertificate; - this.isBridgeV3 = isBridgeV3; + this.useSignifyCaCertificateV2 = useSignifyCaCerteficateV2; } @Override @@ -80,8 +92,9 @@ public X509ExtendedTrustManager getTrustManager() { localTrustManager = PEMTrustManager.getInstanceFromServer("https://" + getHostName()); } else { logger.trace("Use Signify private CA Certificate for Hue Bridges from resources."); - // use Signify private CA Certificate for Hue Bridges from resources - localTrustManager = getInstanceFromResource(isBridgeV3 ? PEM3_FILENAME : PEM_FILENAME); + // use Signify private CA Certificate V1 or V2 for Hue Bridges from resources + localTrustManager = getInstanceFromResource( + useSignifyCaCertificateV2 ? PEM_CACERT_V2_FILENAME : PEM_CACERT_V1_FILENAME); } this.trustManager = localTrustManager; } catch (CertificateException | MalformedURLException e) { diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeMDNSDiscoveryParticipant.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeMDNSDiscoveryParticipant.java index e7fed97af21fe..3ee8b3709816b 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeMDNSDiscoveryParticipant.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeMDNSDiscoveryParticipant.java @@ -56,10 +56,29 @@ @NonNullByDefault public class HueBridgeMDNSDiscoveryParticipant implements MDNSDiscoveryParticipant { + private static final Pattern BSB_MODEL_ID_PATTERN = Pattern.compile("^BSB(\\d{3})$"); + + /** + * Checks if the given model ID is a BSB model and if its version is 003 or above. + * + * @param modelId the model ID to check + * @return true if the model ID is a BSB model with version 003 or above, false otherwise + */ + public static boolean modelIsOrAboveBSB003(@Nullable String modelId) { + if (modelId == null) { + return false; + } + Matcher matcher = BSB_MODEL_ID_PATTERN.matcher(modelId); + if (!matcher.matches()) { + return false; + } + int version = Integer.parseInt(matcher.group(1)); + return version >= 3; + } + private static final String SERVICE_TYPE = "_hue._tcp.local."; private static final String MDNS_PROPERTY_BRIDGE_ID = "bridgeid"; private static final String MDNS_PROPERTY_MODEL_ID = "modelid"; - private static final Pattern BSB_MODEL_ID_PATTERN = Pattern.compile("^BSB(\\d{3})$"); private final Logger logger = LoggerFactory.getLogger(HueBridgeMDNSDiscoveryParticipant.class); protected final ThingRegistry thingRegistry; @@ -164,7 +183,7 @@ private Optional getLegacyBridge(String ipAddress) { String id = service.getPropertyString(MDNS_PROPERTY_BRIDGE_ID); if (id != null && !id.isBlank()) { id = id.toLowerCase(); - if (isOrAboveBSB003(service.getPropertyString(MDNS_PROPERTY_MODEL_ID))) { + if (modelIsOrAboveBSB003(service.getPropertyString(MDNS_PROPERTY_MODEL_ID))) { return new ThingUID(THING_TYPE_BRIDGE_API2, id); } try { @@ -178,15 +197,6 @@ private Optional getLegacyBridge(String ipAddress) { return null; } - private boolean isOrAboveBSB003(@Nullable String modelId) { - Matcher matcher = BSB_MODEL_ID_PATTERN.matcher(modelId); - if (!matcher.matches()) { - return false; - } - int version = Integer.parseInt(matcher.group(1)); - return version >= 3; - } - @Override public long getRemovalGracePeriodSeconds(ServiceInfo service) { return removalGracePeriod; diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java index bad5474308d56..59297d94b3c56 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java @@ -45,6 +45,7 @@ import org.openhab.binding.hue.internal.connection.Clip2Bridge; import org.openhab.binding.hue.internal.connection.HueTlsTrustManagerProvider; import org.openhab.binding.hue.internal.discovery.Clip2ThingDiscoveryService; +import org.openhab.binding.hue.internal.discovery.HueBridgeMDNSDiscoveryParticipant; import org.openhab.binding.hue.internal.exceptions.ApiException; import org.openhab.binding.hue.internal.exceptions.AssetNotLoadedException; import org.openhab.binding.hue.internal.exceptions.HttpUnauthorizedException; @@ -86,8 +87,6 @@ @NonNullByDefault public class Clip2BridgeHandler extends BaseBridgeHandler { - private static final String BRIDGE_V3_MODEL_ID = "BSB003"; // model name of bridge v3 (black bridge) - public static final Set SUPPORTED_THING_TYPES = Set.of(THING_TYPE_BRIDGE_API2); private static final int FAST_SCHEDULE_MILLI_SECONDS = 500; @@ -496,9 +495,10 @@ private void initializeAssets() { return; } - boolean isBridgeV3 = BRIDGE_V3_MODEL_ID.equals(thing.getProperties().get(Thing.PROPERTY_MODEL_ID)); + boolean useSignifyCaCertificateVersion2 = HueBridgeMDNSDiscoveryParticipant + .modelIsOrAboveBSB003(thing.getProperties().get(Thing.PROPERTY_MODEL_ID)); HueTlsTrustManagerProvider trustManagerProvider = new HueTlsTrustManagerProvider(ipAddress + ":443", - config.useSelfSignedCertificate, isBridgeV3); + config.useSelfSignedCertificate, useSignifyCaCertificateVersion2); if (Objects.isNull(trustManagerProvider.getPEMTrustManager())) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, diff --git a/bundles/org.openhab.binding.hue/src/main/resources/huebridge3_cacert.pem b/bundles/org.openhab.binding.hue/src/main/resources/huebridge3_cacert.pem deleted file mode 100644 index 9766019ff495b..0000000000000 --- a/bundles/org.openhab.binding.hue/src/main/resources/huebridge3_cacert.pem +++ /dev/null @@ -1,26 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICMjCCAdigAwIBAgIUO7FSLbaxikuXAljzVaurLXWmFw4wCgYIKoZIzj0EAwIw -OTELMAkGA1UEBhMCTkwxFDASBgNVBAoMC1BoaWxpcHMgSHVlMRQwEgYDVQQDDAty -b290LWJyaWRnZTAiGA8yMDE3MDEwMTAwMDAwMFoYDzIwMzgwMTE5MDMxNDA3WjA5 -MQswCQYDVQQGEwJOTDEUMBIGA1UECgwLUGhpbGlwcyBIdWUxFDASBgNVBAMMC3Jv -b3QtYnJpZGdlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEjNw2tx2AplOf9x86 -aTdvEcL1FU65QDxziKvBpW9XXSIcibAeQiKxegpq8Exbr9v6LBnYbna2VcaK0G22 -jOKkTqOBuTCBtjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNV -HQ4EFgQUZ2ONTFrDT6o8ItRnKfqWKnHFGmQwdAYDVR0jBG0wa4AUZ2ONTFrDT6o8 -ItRnKfqWKnHFGmShPaQ7MDkxCzAJBgNVBAYTAk5MMRQwEgYDVQQKDAtQaGlsaXBz -IEh1ZTEUMBIGA1UEAwwLcm9vdC1icmlkZ2WCFDuxUi22sYpLlwJY81Wrqy11phcO -MAoGCCqGSM49BAMCA0gAMEUCIEBYYEOsa07TH7E5MJnGw557lVkORgit2Rm1h3B2 -sFgDAiEA1Fj/C3AN5psFMjo0//mrQebo0eKd3aWRx+pQY08mk48= ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIBzDCCAXOgAwIBAgICEAAwCgYIKoZIzj0EAwIwPDELMAkGA1UEBhMCTkwxFDAS -BgNVBAoMC1NpZ25pZnkgSHVlMRcwFQYDVQQDDA5IdWUgUm9vdCBDQSAwMTAgFw0y -NTAyMjUwMDAwMDBaGA8yMDUwMTIzMTIzNTk1OVowPDELMAkGA1UEBhMCTkwxFDAS -BgNVBAoMC1NpZ25pZnkgSHVlMRcwFQYDVQQDDA5IdWUgUm9vdCBDQSAwMTBZMBMG -ByqGSM49AgEGCCqGSM49AwEHA0IABFfOO0jfSAUXGQ9kjEDzyBrcMQ3ItyA5krE+ -cyvb1Y3xFti7KlAad8UOnAx0FBLn7HZrlmIwm1QnX0fK3LPM13mjYzBhMB0GA1Ud -DgQWBBTF1pSpsCASX/z0VHLigxU2CAaqoTAfBgNVHSMEGDAWgBTF1pSpsCASX/z0 -VHLigxU2CAaqoTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAKBggq -hkjOPQQDAgNHADBEAiAk7duT+IHbOGO4UUuGLAEpyYejGZK9Z7V9oSfnvuQ5BQIg -IYSgwwxHXm73/JgcU9lAM6c8Bmu3UE3kBIUwBs1qXFw= ------END CERTIFICATE----- \ No newline at end of file diff --git a/bundles/org.openhab.binding.hue/src/main/resources/huebridge_cacert_v2.pem b/bundles/org.openhab.binding.hue/src/main/resources/huebridge_cacert_v2.pem new file mode 100644 index 0000000000000..081a58f82efbf --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/resources/huebridge_cacert_v2.pem @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIBzDCCAXOgAwIBAgICEAAwCgYIKoZIzj0EAwIwPDELMAkGA1UEBhMCTkwxFDAS +BgNVBAoMC1NpZ25pZnkgSHVlMRcwFQYDVQQDDA5IdWUgUm9vdCBDQSAwMTAgFw0y +NTAyMjUwMDAwMDBaGA8yMDUwMTIzMTIzNTk1OVowPDELMAkGA1UEBhMCTkwxFDAS +BgNVBAoMC1NpZ25pZnkgSHVlMRcwFQYDVQQDDA5IdWUgUm9vdCBDQSAwMTBZMBMG +ByqGSM49AgEGCCqGSM49AwEHA0IABFfOO0jfSAUXGQ9kjEDzyBrcMQ3ItyA5krE+ +cyvb1Y3xFti7KlAad8UOnAx0FBLn7HZrlmIwm1QnX0fK3LPM13mjYzBhMB0GA1Ud +DgQWBBTF1pSpsCASX/z0VHLigxU2CAaqoTAfBgNVHSMEGDAWgBTF1pSpsCASX/z0 +VHLigxU2CAaqoTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAKBggq +hkjOPQQDAgNHADBEAiAk7duT+IHbOGO4UUuGLAEpyYejGZK9Z7V9oSfnvuQ5BQIg +IYSgwwxHXm73/JgcU9lAM6c8Bmu3UE3kBIUwBs1qXFw= +-----END CERTIFICATE----- From 49d2db012ccaf33a3b7f10b34f906de51f54cc48 Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Mon, 20 Oct 2025 16:42:04 +0100 Subject: [PATCH 6/7] use trust all trust manager Signed-off-by: Andrew Fiddian-Green --- .../hue/internal/connection/Clip2Bridge.java | 49 ++++++++++++++++--- .../HueTlsTrustManagerProvider.java | 26 ++++++---- 2 files changed, 58 insertions(+), 17 deletions(-) diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/Clip2Bridge.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/Clip2Bridge.java index 2d7b52a92ed42..77123d6fe462f 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/Clip2Bridge.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/Clip2Bridge.java @@ -16,11 +16,18 @@ import java.io.ByteArrayInputStream; import java.io.Closeable; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; +import java.net.HttpURLConnection; import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; @@ -28,7 +35,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Properties; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; @@ -40,6 +46,8 @@ import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; import javax.ws.rs.core.MediaType; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -81,7 +89,7 @@ import org.openhab.binding.hue.internal.exceptions.HttpUnauthorizedException; import org.openhab.binding.hue.internal.handler.Clip2BridgeHandler; import org.openhab.core.io.net.http.HttpClientFactory; -import org.openhab.core.io.net.http.HttpUtil; +import org.openhab.core.io.net.http.TrustAllTrustManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -532,11 +540,38 @@ public void close() { * @throws NumberFormatException if the bridge firmware version is invalid. */ public static boolean isClip2Supported(String hostName) throws IOException { - String response; - Properties headers = new Properties(); - headers.put(HttpHeader.ACCEPT, MediaType.APPLICATION_JSON); - response = HttpUtil.executeUrl("GET", String.format(FORMAT_URL_CONFIG, hostName), headers, null, null, - TIMEOUT_SECONDS * 1000); + String response = null; + try { + URL url = new URI(String.format(FORMAT_URL_CONFIG, hostName)).toURL(); + HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection(); + /* + * TODO we manually check if the bridge redirects to HTTPS, and if so, since v3 bridges + * currently don't provide a full certificate chain we force use of a TrustAllTrustManager + */ + httpConnection.setInstanceFollowRedirects(false); + int status = httpConnection.getResponseCode(); + if (status == 301 || status == 302) { + String redirectUrl = httpConnection.getHeaderField("Location"); + if (redirectUrl.startsWith("https://")) { + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, new TrustAllTrustManager[] { TrustAllTrustManager.getInstance() }, null); + HttpsURLConnection httpsConnection = (HttpsURLConnection) new URI(redirectUrl).toURL() + .openConnection(); + httpsConnection.setSSLSocketFactory(sslContext.getSocketFactory()); + try (InputStream in = httpsConnection.getInputStream()) { + response = new String(in.readAllBytes()); + } + } + } + if (response == null) { + try (InputStream in = httpConnection.getInputStream()) { + response = new String(in.readAllBytes()); + } + } + } catch (NoSuchAlgorithmException | KeyManagementException | URISyntaxException e) { + throw new IOException("isClip2Supported() error connecting to bridge", e); + } + BridgeConfig config = new Gson().fromJson(response, BridgeConfig.class); if (Objects.nonNull(config)) { String swVersion = config.swversion; diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/HueTlsTrustManagerProvider.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/HueTlsTrustManagerProvider.java index 1785b827b3ff5..00f66aede549e 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/HueTlsTrustManagerProvider.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/HueTlsTrustManagerProvider.java @@ -42,11 +42,11 @@ public class HueTlsTrustManagerProvider implements TlsTrustManagerProvider { private static final String PEM_CACERT_V2_FILENAME = "huebridge_cacert_v2.pem"; private final String hostname; private final boolean useSelfSignedCertificate; - private final boolean useSignifyCaCertificateV2; + private final boolean isBridgeV3orHigher; private final Logger logger = LoggerFactory.getLogger(HueTlsTrustManagerProvider.class); - private @Nullable PEMTrustManager trustManager; + private @Nullable X509ExtendedTrustManager trustManager; /** * Creates a new instance of {@link HueTlsTrustManagerProvider}. @@ -56,14 +56,13 @@ public class HueTlsTrustManagerProvider implements TlsTrustManagerProvider { * @param hostname the hostname of the Hue Bridge * @param useSelfSignedCertificate true, to use the self-signed certificate downloaded from the Hue Bridge; * false, to use the Signify private CA Certificate V1 or V2 for Hue Bridges from resources - * @param useSignifyCaCerteficateV2 true, to use the 'Signify private CA Certificate V2 for Hue Bridges'; + * @param isBridgeV3orHigher true, to use the 'Signify private CA Certificate V2 for Hue Bridges'; * false, to use the 'Signify private CA Certificate V1 for Hue Bridges' */ - public HueTlsTrustManagerProvider(String hostname, boolean useSelfSignedCertificate, - boolean useSignifyCaCerteficateV2) { + public HueTlsTrustManagerProvider(String hostname, boolean useSelfSignedCertificate, boolean isBridgeV3orHigher) { this.hostname = hostname; this.useSelfSignedCertificate = useSelfSignedCertificate; - this.useSignifyCaCertificateV2 = useSignifyCaCerteficateV2; + this.isBridgeV3orHigher = isBridgeV3orHigher; } @Override @@ -73,18 +72,25 @@ public String getHostName() { @Override public X509ExtendedTrustManager getTrustManager() { - PEMTrustManager localTrustManager = getPEMTrustManager(); + X509ExtendedTrustManager localTrustManager = getPEMTrustManager(); if (localTrustManager == null) { logger.error("Cannot get the PEM certificate - returning a TrustAllTrustManager"); } return localTrustManager != null ? localTrustManager : TrustAllTrustManager.getInstance(); } - public @Nullable PEMTrustManager getPEMTrustManager() { - PEMTrustManager localTrustManager = trustManager; + public @Nullable X509ExtendedTrustManager getPEMTrustManager() { + X509ExtendedTrustManager localTrustManager = trustManager; if (localTrustManager != null) { return localTrustManager; } + + // TODO V3 bridges currently don't provide the full certificate chain (missing intermediate certificate) + if (isBridgeV3orHigher) { + logger.error("Hue V3 Bridge has incomplete PEM certificate chains .. default to a TrustAllTrustManager"); + return TrustAllTrustManager.getInstance(); + } + try { if (useSelfSignedCertificate) { logger.trace("Use self-signed certificate downloaded from Hue Bridge."); @@ -94,7 +100,7 @@ public X509ExtendedTrustManager getTrustManager() { logger.trace("Use Signify private CA Certificate for Hue Bridges from resources."); // use Signify private CA Certificate V1 or V2 for Hue Bridges from resources localTrustManager = getInstanceFromResource( - useSignifyCaCertificateV2 ? PEM_CACERT_V2_FILENAME : PEM_CACERT_V1_FILENAME); + isBridgeV3orHigher ? PEM_CACERT_V2_FILENAME : PEM_CACERT_V1_FILENAME); } this.trustManager = localTrustManager; } catch (CertificateException | MalformedURLException e) { From f228ea212fc78c6852d1af22449c95775cc53e53 Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Mon, 20 Oct 2025 17:11:53 +0100 Subject: [PATCH 7/7] copilot fixes Signed-off-by: Andrew Fiddian-Green --- .../hue/internal/connection/Clip2Bridge.java | 20 +++++++++++++------ .../HueTlsTrustManagerProvider.java | 9 ++++++--- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/Clip2Bridge.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/Clip2Bridge.java index 77123d6fe462f..779fda3e37fea 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/Clip2Bridge.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/Clip2Bridge.java @@ -541,9 +541,11 @@ public void close() { */ public static boolean isClip2Supported(String hostName) throws IOException { String response = null; + HttpURLConnection httpConnection = null; + HttpsURLConnection httpsConnection = null; try { URL url = new URI(String.format(FORMAT_URL_CONFIG, hostName)).toURL(); - HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection(); + httpConnection = (HttpURLConnection) url.openConnection(); /* * TODO we manually check if the bridge redirects to HTTPS, and if so, since v3 bridges * currently don't provide a full certificate chain we force use of a TrustAllTrustManager @@ -552,24 +554,30 @@ public static boolean isClip2Supported(String hostName) throws IOException { int status = httpConnection.getResponseCode(); if (status == 301 || status == 302) { String redirectUrl = httpConnection.getHeaderField("Location"); - if (redirectUrl.startsWith("https://")) { + if (redirectUrl != null && redirectUrl.startsWith("https://")) { SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, new TrustAllTrustManager[] { TrustAllTrustManager.getInstance() }, null); - HttpsURLConnection httpsConnection = (HttpsURLConnection) new URI(redirectUrl).toURL() - .openConnection(); + httpsConnection = (HttpsURLConnection) new URI(redirectUrl).toURL().openConnection(); httpsConnection.setSSLSocketFactory(sslContext.getSocketFactory()); try (InputStream in = httpsConnection.getInputStream()) { - response = new String(in.readAllBytes()); + response = new String(in.readAllBytes(), StandardCharsets.UTF_8); } } } if (response == null) { try (InputStream in = httpConnection.getInputStream()) { - response = new String(in.readAllBytes()); + response = new String(in.readAllBytes(), StandardCharsets.UTF_8); } } } catch (NoSuchAlgorithmException | KeyManagementException | URISyntaxException e) { throw new IOException("isClip2Supported() error connecting to bridge", e); + } finally { + if (httpConnection != null) { + httpConnection.disconnect(); + } + if (httpsConnection != null) { + httpsConnection.disconnect(); + } } BridgeConfig config = new Gson().fromJson(response, BridgeConfig.class); diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/HueTlsTrustManagerProvider.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/HueTlsTrustManagerProvider.java index 00f66aede549e..9e98a16bf3603 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/HueTlsTrustManagerProvider.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/HueTlsTrustManagerProvider.java @@ -50,8 +50,11 @@ public class HueTlsTrustManagerProvider implements TlsTrustManagerProvider { /** * Creates a new instance of {@link HueTlsTrustManagerProvider}. - * See {@link https://developers.meethue.com/develop/application-design-guidance/using-https/} for more - * details about 'Signify private CA Certificates V1 and V2 for Hue Bridges'. + * + * See the documentation for more details about 'Signify private CA Certificates V1 and V2 for Hue Bridges'. + * + * @see https://developers.meethue.com/develop/application-design-guidance/using-https/ * * @param hostname the hostname of the Hue Bridge * @param useSelfSignedCertificate true, to use the self-signed certificate downloaded from the Hue Bridge; @@ -87,7 +90,7 @@ public X509ExtendedTrustManager getTrustManager() { // TODO V3 bridges currently don't provide the full certificate chain (missing intermediate certificate) if (isBridgeV3orHigher) { - logger.error("Hue V3 Bridge has incomplete PEM certificate chains .. default to a TrustAllTrustManager"); + logger.error("Hue V3 Bridge has incomplete PEM certificate chains - defaulting to a TrustAllTrustManager"); return TrustAllTrustManager.getInstance(); }