]> git.basschouten.com Git - openhab-addons.git/blob
4683d29cffcb8158705c901f630b7a800431a29e
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.boschshc.internal.devices.bridge;
14
15 import java.io.BufferedInputStream;
16 import java.io.File;
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;
35
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;
51
52 /**
53  * SSL context utility.
54  *
55  * @author Gerd Zanker - Initial contribution
56  */
57 @NonNullByDefault
58 public class BoschSslUtil {
59
60     private static final String OSS_OPENHAB_BINDING = "oss_openhab_binding";
61     private static final String KEYSTORE_PASSWORD = "openhab";
62
63     private final Logger logger = LoggerFactory.getLogger(BoschSslUtil.class);
64
65     private final String boschShcServerID;
66     private final String keystorePath;
67
68     /**
69      * Returns unique ID for this Bosch SmartHomeController client.
70      * 
71      * @return unique string containing the openhab UUID.
72      */
73     public static String getBoschShcClientId() {
74         return OSS_OPENHAB_BINDING + "_" + InstanceUUID.get();
75     }
76
77     /**
78      * Returns ID for passed Bosch SmartHomeController server.
79      * 
80      * @param shcServerID the ip address of the SHC server
81      * @return unique string containing the server id
82      */
83     public static String getBoschShcServerId(String shcServerID) {
84         return OSS_OPENHAB_BINDING + "_" + shcServerID;
85     }
86
87     /**
88      * Constructor
89      * 
90      * @param boschShcServerID the ip address of the SHC server
91      */
92     public BoschSslUtil(String boschShcServerID) {
93         this.boschShcServerID = boschShcServerID;
94         this.keystorePath = getKeystorePath();
95     }
96
97     /// Returns unique ID for Bosch SmartHomeController server.
98     public String getBoschShcServerId() {
99         return BoschSslUtil.getBoschShcServerId(boschShcServerID);
100     }
101
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();
105     }
106
107     public SslContextFactory getSslContextFactory() throws PairingFailedException {
108         // Instantiate and configure the SslContextFactory
109         SslContextFactory sslContextFactory = new SslContextFactory.Client.Client(true); // Accept all certificates
110
111         // during pairing the cert from this keystore is accessed by HTTP client via name
112         sslContextFactory.setKeyStore(getKeyStoreAndCreateIfNecessary());
113
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);
118
119         // Bosch is using a self signed certificate
120         sslContextFactory.setTrustAll(true);
121         sslContextFactory.setValidateCerts(false);
122         sslContextFactory.setValidatePeerCerts(false);
123         sslContextFactory.setEndpointIdentificationAlgorithm(null);
124
125         return sslContextFactory;
126     }
127
128     public KeyStore getKeyStoreAndCreateIfNecessary() throws PairingFailedException {
129         try {
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);
135             } else {
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());
140                 }
141                 logger.debug("Using existing keystore {}", keystorePath);
142                 return keyStore;
143             }
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);
148         }
149     }
150
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);
159
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
164         );
165         // and sign it
166         ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256WithRSA").build(keyPair.getPrivate());
167         return new JcaX509CertificateConverter().setProvider(new BouncyCastleProvider())
168                 .getCertificate(certificateBuilder.build(contentSigner));
169     }
170
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);
176
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();
182
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));
187         signer.sign();
188
189         X509Certificate cert = generateClientCertificate(keyPair);
190
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 });
194
195         // add Bosch Certs
196         CertificateFactory cf = CertificateFactory.getInstance("X.509");
197
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);
203         }
204
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);
210         }
211
212         logger.debug("Storing keystore to file {}", keystore);
213         try (FileOutputStream keystoreStream = new FileOutputStream(keystore)) {
214             keyStore.store(keystoreStream, KEYSTORE_PASSWORD.toCharArray());
215         }
216
217         return keyStore;
218     }
219 }