1212 */
1313package org .openhab .binding .hue .internal .connection ;
1414
15+ import java .io .ByteArrayInputStream ;
1516import java .io .IOException ;
1617import java .io .InputStream ;
1718import java .net .MalformedURLException ;
1819import java .net .URL ;
1920import java .nio .charset .StandardCharsets ;
21+ import java .security .KeyStore ;
22+ import java .security .cert .Certificate ;
2023import java .security .cert .CertificateException ;
24+ import java .security .cert .CertificateFactory ;
25+ import java .util .Collection ;
2126
27+ import javax .net .ssl .TrustManager ;
28+ import javax .net .ssl .TrustManagerFactory ;
2229import javax .net .ssl .X509ExtendedTrustManager ;
2330
2431import org .eclipse .jdt .annotation .NonNullByDefault ;
2532import org .eclipse .jdt .annotation .Nullable ;
2633import org .openhab .core .io .net .http .PEMTrustManager ;
27- import org .openhab .core .io .net .http .PEMTrustManager .CertificateInstantiationException ;
2834import org .openhab .core .io .net .http .TlsTrustManagerProvider ;
2935import org .openhab .core .io .net .http .TrustAllTrustManager ;
3036import org .slf4j .Logger ;
3137import org .slf4j .LoggerFactory ;
3238
3339/**
34- * Provides a {@link PEMTrustManager } to allow secure connections to any Hue Bridge.
40+ * Provides a {@link X509ExtendedTrustManager } to allow secure connections to any Hue Bridge.
3541 *
3642 * @author Christoph Weitkamp - Initial Contribution
43+ * @author Andrew Fiddian-Green - Add support for intermediate certificates on V3 bridges
3744 */
3845@ NonNullByDefault
3946public class HueTlsTrustManagerProvider implements TlsTrustManagerProvider {
4047
41- private static final String PEM_CACERT_V1_FILENAME = "huebridge_cacert.pem" ;
42- private static final String PEM_CACERT_V2_FILENAME = "huebridge_cacert_v2.pem" ;
48+ private static final String PEM_CACERT_FILENAME = "huebridge_cacert.pem" ;
4349 private final String hostname ;
4450 private final boolean useSelfSignedCertificate ;
45- private final boolean isBridgeV3orHigher ;
4651
4752 private final Logger logger = LoggerFactory .getLogger (HueTlsTrustManagerProvider .class );
4853
@@ -51,21 +56,18 @@ public class HueTlsTrustManagerProvider implements TlsTrustManagerProvider {
5156 /**
5257 * Creates a new instance of {@link HueTlsTrustManagerProvider}.
5358 *
54- * See the documentation for more details about 'Signify private CA Certificates V1 and V2 for Hue Bridges'.
59+ * See the documentation for more details about 'Signify private CA Certificates for Hue Bridges'.
5560 *
5661 * @see <a href=
5762 * "https://developers.meethue.com/develop/application-design-guidance/using-https/">https://developers.meethue.com/develop/application-design-guidance/using-https/</a>
5863 *
59- * @param hostname the hostname of the Hue Bridge
64+ * @param hostname the host name of the Hue Bridge
6065 * @param useSelfSignedCertificate true, to use the self-signed certificate downloaded from the Hue Bridge;
61- * false, to use the Signify private CA Certificate V1 or V2 for Hue Bridges from resources
62- * @param isBridgeV3orHigher true, to use the 'Signify private CA Certificate V2 for Hue Bridges';
63- * false, to use the 'Signify private CA Certificate V1 for Hue Bridges'
66+ * false, to use the Signify private CA Certificate(s) for Hue Bridges from resources
6467 */
65- public HueTlsTrustManagerProvider (String hostname , boolean useSelfSignedCertificate , boolean isBridgeV3orHigher ) {
68+ public HueTlsTrustManagerProvider (String hostname , boolean useSelfSignedCertificate ) {
6669 this .hostname = hostname ;
6770 this .useSelfSignedCertificate = useSelfSignedCertificate ;
68- this .isBridgeV3orHigher = isBridgeV3orHigher ;
6971 }
7072
7173 @ Override
@@ -87,50 +89,80 @@ public X509ExtendedTrustManager getTrustManager() {
8789 if (localTrustManager != null ) {
8890 return localTrustManager ;
8991 }
90-
91- // TODO V3 bridges currently don't provide the full certificate chain (missing intermediate certificate)
92- if (isBridgeV3orHigher ) {
93- logger .error ("Hue V3 Bridge has incomplete PEM certificate chains - defaulting to a TrustAllTrustManager" );
94- return TrustAllTrustManager .getInstance ();
95- }
96-
9792 try {
9893 if (useSelfSignedCertificate ) {
9994 logger .trace ("Use self-signed certificate downloaded from Hue Bridge." );
10095 // use self-signed certificate downloaded from Hue Bridge
10196 localTrustManager = PEMTrustManager .getInstanceFromServer ("https://" + getHostName ());
10297 } else {
103- logger .trace ("Use Signify private CA Certificate for Hue Bridges from resources." );
104- // use Signify private CA Certificate V1 or V2 for Hue Bridges from resources
105- localTrustManager = getInstanceFromResource (
106- isBridgeV3orHigher ? PEM_CACERT_V2_FILENAME : PEM_CACERT_V1_FILENAME );
98+ logger .trace ("Use Signify private CA Certificate(s) for Hue Bridges from resources." );
99+ // use Signify private CA Certificate(s) for Hue Bridges from resources
100+ localTrustManager = getInstanceFromResource (PEM_CACERT_FILENAME );
107101 }
108102 this .trustManager = localTrustManager ;
109103 } catch (CertificateException | MalformedURLException e ) {
110- logger .debug ("An unexpected exception occurred: {}" , e .getMessage (), e );
104+ logger .warn ("An unexpected exception occurred: {}" , e .getMessage (), e );
111105 }
112106 return localTrustManager ;
113107 }
114108
115109 /**
116- * Creates a {@link PEMTrustManager} instance by reading the PEM certificate from the given file.
117- * This is useful if you have a private CA Certificate stored in a file.
110+ * Creates a {@link X509ExtendedTrustManager} instance by reading one or more PEM certificates from the given
111+ * file. The returned trust manager will trust all certificates that are signed by any of the certificates in
112+ * the PEM file, including certificates with intermediates. This is useful if you have private CA Certificate(s)
113+ * stored in a file.
118114 *
119- * @param fileName name to the PEM file located in the resources folder
120- * @return a {@link PEMTrustManager } instance
121- * @throws CertificateInstantiationException
115+ * @param fileName name of the PEM file located in the resources folder
116+ * @return a {@link X509ExtendedTrustManager } instance
117+ * @throws CertificateException
122118 */
123- private PEMTrustManager getInstanceFromResource (String fileName ) throws CertificateException {
124- String pemCert = readPEMCertificateStringFromResource (fileName );
125- if (pemCert != null ) {
126- return new PEMTrustManager (pemCert );
119+ private X509ExtendedTrustManager getInstanceFromResource (String fileName ) throws CertificateException {
120+ String certificatesString = readPEMCertificatesStringFromResource (fileName );
121+ if (certificatesString == null ) {
122+ throw new CertificateException ("Certificate resource '" + fileName + "' not found or not accessible." );
123+ }
124+ try {
125+ CertificateFactory certificateFactory = CertificateFactory .getInstance ("X.509" );
126+ // load all certificates from the PEM file
127+ Collection <? extends Certificate > certificates ;
128+ try (InputStream input = new ByteArrayInputStream (certificatesString .getBytes (StandardCharsets .UTF_8 ))) {
129+ certificates = certificateFactory .generateCertificates (input );
130+ }
131+ if (certificates .isEmpty ()) {
132+ throw new CertificateException ("No certificates found in " + fileName );
133+ }
134+ // build a key store containing all the certificates
135+ KeyStore keyStore = KeyStore .getInstance (KeyStore .getDefaultType ());
136+ keyStore .load (null , null );
137+ int index = 0 ;
138+ for (Certificate cert : certificates ) {
139+ keyStore .setCertificateEntry ("cert-" + index ++, cert );
140+ }
141+ // build a trust manager from this key store
142+ TrustManagerFactory trustManagerFactory = TrustManagerFactory
143+ .getInstance (TrustManagerFactory .getDefaultAlgorithm ());
144+ trustManagerFactory .init (keyStore );
145+ for (TrustManager trustManager : trustManagerFactory .getTrustManagers ()) {
146+ if (trustManager instanceof X509ExtendedTrustManager x509 ) {
147+ return x509 ;
148+ }
149+ }
150+ throw new CertificateException ("No X509ExtendedTrustManager available." );
151+ } catch (Exception e ) {
152+ throw new CertificateException ("Failed to load certificates: " + e .getMessage (), e );
127153 }
128- throw new CertificateInstantiationException (
129- String .format ("Certificate resource '%s' not found or not accessible." , fileName ));
130154 }
131155
132- private @ Nullable String readPEMCertificateStringFromResource (String fileName ) {
133- URL resource = Thread .currentThread ().getContextClassLoader ().getResource (fileName );
156+ /**
157+ * Reads the content of a PEM file from the resources folder and returns it as a string. It may contain multiple
158+ * certificates, e.g. a certificate chain with intermediate certificates. If the file is not found or cannot be
159+ * read, null is returned.
160+ *
161+ * @param fileName name of the PEM file located in the resources folder
162+ * @return the content of the PEM file as a string, or null if the file is not found or cannot be read
163+ */
164+ private @ Nullable String readPEMCertificatesStringFromResource (String fileName ) {
165+ URL resource = HueTlsTrustManagerProvider .class .getClassLoader ().getResource (fileName );
134166 if (resource != null ) {
135167 try (InputStream certInputStream = resource .openStream ()) {
136168 return new String (certInputStream .readAllBytes (), StandardCharsets .UTF_8 );
0 commit comments