2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.boschshc.internal.devices.bridge;
15 import java.io.BufferedInputStream;
17 import java.io.FileInputStream;
18 import java.io.FileOutputStream;
19 import java.io.IOException;
20 import java.math.BigInteger;
21 import java.nio.charset.StandardCharsets;
22 import java.nio.file.Paths;
23 import java.security.GeneralSecurityException;
24 import java.security.KeyPair;
25 import java.security.KeyPairGenerator;
26 import java.security.KeyStore;
27 import java.security.Security;
28 import java.security.Signature;
29 import java.security.cert.Certificate;
30 import java.security.cert.CertificateFactory;
31 import java.security.cert.X509Certificate;
32 import java.time.Duration;
33 import java.time.Instant;
34 import java.util.Date;
36 import org.bouncycastle.asn1.x500.X500Name;
37 import org.bouncycastle.cert.X509v3CertificateBuilder;
38 import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
39 import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
40 import org.bouncycastle.jce.provider.BouncyCastleProvider;
41 import org.bouncycastle.operator.ContentSigner;
42 import org.bouncycastle.operator.OperatorCreationException;
43 import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
44 import org.eclipse.jdt.annotation.NonNullByDefault;
45 import org.eclipse.jetty.util.ssl.SslContextFactory;
46 import org.openhab.binding.boschshc.internal.exceptions.PairingFailedException;
47 import org.openhab.core.OpenHAB;
48 import org.openhab.core.id.InstanceUUID;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
53 * SSL context utility.
55 * @author Gerd Zanker - Initial contribution
58 public class BoschSslUtil {
60 private static final String OSS_OPENHAB_BINDING = "oss_openhab_binding";
61 private static final String KEYSTORE_PASSWORD = "openhab";
63 private final Logger logger = LoggerFactory.getLogger(BoschSslUtil.class);
65 private final String boschShcServerID;
66 private final String keystorePath;
69 * Returns unique ID for this Bosch SmartHomeController client.
71 * @return unique string containing the openhab UUID.
73 public static String getBoschShcClientId() {
74 return OSS_OPENHAB_BINDING + "_" + InstanceUUID.get();
78 * Returns ID for passed Bosch SmartHomeController server.
80 * @param shcServerID the ip address of the SHC server
81 * @return unique string containing the server id
83 public static String getBoschShcServerId(String shcServerID) {
84 return OSS_OPENHAB_BINDING + "_" + shcServerID;
90 * @param boschShcServerID the ip address of the SHC server
92 public BoschSslUtil(String boschShcServerID) {
93 this.boschShcServerID = boschShcServerID;
94 this.keystorePath = getKeystorePath();
97 /// Returns unique ID for Bosch SmartHomeController server.
98 public String getBoschShcServerId() {
99 return BoschSslUtil.getBoschShcServerId(boschShcServerID);
102 /// Returns the unique keystore for each Bosch Smart Home Controller server.
103 public String getKeystorePath() {
104 return Paths.get(OpenHAB.getUserDataFolder(), "etc", getBoschShcServerId() + ".jks").toString();
107 public SslContextFactory getSslContextFactory() throws PairingFailedException {
108 // Instantiate and configure the SslContextFactory
109 SslContextFactory sslContextFactory = new SslContextFactory.Client.Client(true); // Accept all certificates
111 // during pairing the cert from this keystore is accessed by HTTP client via name
112 sslContextFactory.setKeyStore(getKeyStoreAndCreateIfNecessary());
114 // Keystore for managing the keys that have been used to pair with the SHC
115 // https://www.eclipse.org/jetty/javadoc/9.4.12.v20180830/org/eclipse/jetty/util/ssl/SslContextFactory.html
116 sslContextFactory.setKeyStorePath(keystorePath);
117 sslContextFactory.setKeyStorePassword(KEYSTORE_PASSWORD);
119 // Bosch is using a self signed certificate
120 sslContextFactory.setTrustAll(true);
121 sslContextFactory.setValidateCerts(false);
122 sslContextFactory.setValidatePeerCerts(false);
123 sslContextFactory.setEndpointIdentificationAlgorithm(null);
125 return sslContextFactory;
128 public KeyStore getKeyStoreAndCreateIfNecessary() throws PairingFailedException {
130 File file = new File(keystorePath);
131 if (!file.exists()) {
132 // create new keystore
133 logger.info("Creating new keystore {} because it doesn't exist.", keystorePath);
134 return createKeyStore(keystorePath);
136 // load keystore as a first check
137 KeyStore keyStore = KeyStore.getInstance("JKS");
138 try (FileInputStream keystoreStream = new FileInputStream(file)) {
139 keyStore.load(keystoreStream, KEYSTORE_PASSWORD.toCharArray());
141 logger.debug("Using existing keystore {}", keystorePath);
144 } catch (OperatorCreationException | GeneralSecurityException | IOException e) {
145 logger.debug("Exception during keystore creation {}", e.getMessage());
146 throw new PairingFailedException("Can not create or load keystore file: " + keystorePath
147 + ". Check path, write access and JKS content.", e);
151 private X509Certificate generateClientCertificate(KeyPair keyPair)
152 throws GeneralSecurityException, OperatorCreationException {
153 final String dirName = "CN=" + getBoschShcClientId() + ", O=openHAB, L=None, ST=None, C=None";
154 logger.debug("Creating a new self signed certificate: {}", dirName);
155 final Instant now = Instant.now();
156 final Date notBefore = Date.from(now);
157 final Date notAfter = Date.from(now.plus(Duration.ofDays(365 * 10)));
158 X500Name name = new X500Name(dirName);
160 // create the certificate
161 X509v3CertificateBuilder certificateBuilder = new JcaX509v3CertificateBuilder(name, // Issuer
162 BigInteger.valueOf(now.toEpochMilli()), notBefore, notAfter, name, // Subject
163 keyPair.getPublic() // Public key to be associated with the certificate
166 ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256WithRSA").build(keyPair.getPrivate());
167 return new JcaX509CertificateConverter().setProvider(new BouncyCastleProvider())
168 .getCertificate(certificateBuilder.build(contentSigner));
171 private KeyStore createKeyStore(String keystore)
172 throws IOException, OperatorCreationException, GeneralSecurityException {
173 // create a new keystore
174 KeyStore keyStore = KeyStore.getInstance("JKS");
175 keyStore.load(null, null);
177 // create new key pair for BoschSHC binding
178 logger.debug("Creating new keypair");
179 KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
180 kpg.initialize(2048);
181 KeyPair keyPair = kpg.generateKeyPair();
183 Security.addProvider(new BouncyCastleProvider());
184 Signature signer = Signature.getInstance("SHA256withRSA", "BC");
185 signer.initSign(keyPair.getPrivate());
186 signer.update("Hello openHAB".getBytes(StandardCharsets.UTF_8));
189 X509Certificate cert = generateClientCertificate(keyPair);
191 logger.debug("Adding keyEntry '{}' with self signed certificate to keystore", getBoschShcServerId());
192 keyStore.setKeyEntry(getBoschShcServerId(), keyPair.getPrivate(), KEYSTORE_PASSWORD.toCharArray(),
193 new Certificate[] { cert });
196 CertificateFactory cf = CertificateFactory.getInstance("X.509");
198 logger.debug("Adding Issuing CA to keystore");
199 try (BufferedInputStream streamIssuingCA = new BufferedInputStream(
200 this.getClass().getResourceAsStream("SmartHomeControllerIssuingCA.pem"))) {
201 Certificate certIssuingCA = cf.generateCertificate(streamIssuingCA);
202 keyStore.setCertificateEntry("Smart Home Controller Issuing CA", certIssuingCA);
205 logger.debug("Adding root CA to keystore");
206 try (BufferedInputStream streamRootCa = new BufferedInputStream(
207 this.getClass().getResourceAsStream("SmartHomeControllerProductiveRootCA.pem"))) {
208 Certificate certRooCA = cf.generateCertificate(streamRootCa);
209 keyStore.setCertificateEntry("Smart Home Controller Productive Root CA", certRooCA);
212 logger.debug("Storing keystore to file {}", keystore);
213 try (FileOutputStream keystoreStream = new FileOutputStream(keystore)) {
214 keyStore.store(keystoreStream, KEYSTORE_PASSWORD.toCharArray());