Commit 4aabf936f1ee79324c25693f5f311030e51e9841

Authored by Valerii Sosliuk
1 parent e25bd4f0

X509 certificate support implemented

@@ -51,6 +51,8 @@ public class DefaultDeviceAuthService implements DeviceAuthService { @@ -51,6 +51,8 @@ public class DefaultDeviceAuthService implements DeviceAuthService {
51 // Credentials ID matches Credentials value in this 51 // Credentials ID matches Credentials value in this
52 // primitive case; 52 // primitive case;
53 return DeviceAuthResult.of(credentials.getDeviceId()); 53 return DeviceAuthResult.of(credentials.getDeviceId());
  54 + case X509_CERTIFICATE:
  55 + return DeviceAuthResult.of(credentials.getDeviceId());
54 default: 56 default:
55 return DeviceAuthResult.of("Credentials Type is not supported yet!"); 57 return DeviceAuthResult.of("Credentials Type is not supported yet!");
56 } 58 }
@@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.security; @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.security;
17 17
18 public enum DeviceCredentialsType { 18 public enum DeviceCredentialsType {
19 19
20 - ACCESS_TOKEN 20 + ACCESS_TOKEN,
  21 + X509_CERTIFICATE
21 22
22 } 23 }
@@ -20,7 +20,6 @@ public class DeviceTokenCredentials implements DeviceCredentialsFilter { @@ -20,7 +20,6 @@ public class DeviceTokenCredentials implements DeviceCredentialsFilter {
20 private final String token; 20 private final String token;
21 21
22 public DeviceTokenCredentials(String token) { 22 public DeviceTokenCredentials(String token) {
23 - super();  
24 this.token = token; 23 this.token = token;
25 } 24 }
26 25
  1 +/**
  2 + * Copyright © 2016 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.common.data.security;
  17 +
  18 +/**
  19 + * @author Valerii Sosliuk
  20 + */
  21 +public class DeviceX509Credentials implements DeviceCredentialsFilter {
  22 +
  23 + private final String sha3Hash;
  24 +
  25 + public DeviceX509Credentials(String sha3Hash) {
  26 + this.sha3Hash = sha3Hash;
  27 + }
  28 +
  29 + @Override
  30 + public String getCredentialsId() { return sha3Hash; }
  31 +
  32 + @Override
  33 + public DeviceCredentialsType getCredentialsType() { return DeviceCredentialsType.X509_CERTIFICATE; }
  34 +
  35 + @Override
  36 + public String toString() {
  37 + return "DeviceX509Credentials [SHA3=" + sha3Hash + "]";
  38 + }
  39 +}
@@ -146,6 +146,10 @@ @@ -146,6 +146,10 @@
146 <groupId>org.springframework.boot</groupId> 146 <groupId>org.springframework.boot</groupId>
147 <artifactId>spring-boot-autoconfigure</artifactId> 147 <artifactId>spring-boot-autoconfigure</artifactId>
148 </dependency> 148 </dependency>
  149 + <dependency>
  150 + <groupId>org.bouncycastle</groupId>
  151 + <artifactId>bcprov-jdk15on</artifactId>
  152 + </dependency>
149 </dependencies> 153 </dependencies>
150 <build> 154 <build>
151 <plugins> 155 <plugins>
@@ -18,9 +18,7 @@ package org.thingsboard.server.dao; @@ -18,9 +18,7 @@ package org.thingsboard.server.dao;
18 import java.util.ArrayList; 18 import java.util.ArrayList;
19 import java.util.Collection; 19 import java.util.Collection;
20 import java.util.Collections; 20 import java.util.Collections;
21 -import java.util.HashSet;  
22 import java.util.List; 21 import java.util.List;
23 -import java.util.Set;  
24 import java.util.UUID; 22 import java.util.UUID;
25 23
26 import org.thingsboard.server.common.data.id.UUIDBased; 24 import org.thingsboard.server.common.data.id.UUIDBased;
  1 +/**
  2 + * Copyright © 2016 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao;
  17 +
  18 +import lombok.extern.slf4j.Slf4j;
  19 +import org.bouncycastle.crypto.digests.SHA3Digest;
  20 +import org.bouncycastle.pqc.math.linearalgebra.ByteUtils;
  21 +/**
  22 + * @author Valerii Sosliuk
  23 + */
  24 +@Slf4j
  25 +public class EncryptionUtil {
  26 +
  27 + private EncryptionUtil() {
  28 + }
  29 +
  30 + public static String getSha3Hash(String data) {
  31 + String trimmedData = data.replaceAll("\n","").replaceAll("\r","");
  32 + byte[] dataBytes = trimmedData.getBytes();
  33 + SHA3Digest md = new SHA3Digest(256);
  34 + md.reset();
  35 + md.update(dataBytes, 0, dataBytes.length);
  36 + byte[] hashedBytes = new byte[256 / 8];
  37 + md.doFinal(hashedBytes, 0);
  38 + String sha3Hash = ByteUtils.toHexString(hashedBytes);
  39 + return sha3Hash;
  40 + }
  41 +}
@@ -23,6 +23,8 @@ import org.springframework.util.StringUtils; @@ -23,6 +23,8 @@ import org.springframework.util.StringUtils;
23 import org.thingsboard.server.common.data.Device; 23 import org.thingsboard.server.common.data.Device;
24 import org.thingsboard.server.common.data.id.DeviceId; 24 import org.thingsboard.server.common.data.id.DeviceId;
25 import org.thingsboard.server.common.data.security.DeviceCredentials; 25 import org.thingsboard.server.common.data.security.DeviceCredentials;
  26 +import org.thingsboard.server.common.data.security.DeviceCredentialsType;
  27 +import org.thingsboard.server.dao.EncryptionUtil;
26 import org.thingsboard.server.dao.exception.DataValidationException; 28 import org.thingsboard.server.dao.exception.DataValidationException;
27 import org.thingsboard.server.dao.model.DeviceCredentialsEntity; 29 import org.thingsboard.server.dao.model.DeviceCredentialsEntity;
28 import org.thingsboard.server.dao.service.DataValidator; 30 import org.thingsboard.server.dao.service.DataValidator;
@@ -70,11 +72,19 @@ public class DeviceCredentialsServiceImpl implements DeviceCredentialsService { @@ -70,11 +72,19 @@ public class DeviceCredentialsServiceImpl implements DeviceCredentialsService {
70 } 72 }
71 73
72 private DeviceCredentials saveOrUpdare(DeviceCredentials deviceCredentials) { 74 private DeviceCredentials saveOrUpdare(DeviceCredentials deviceCredentials) {
  75 + if (deviceCredentials.getCredentialsType() == DeviceCredentialsType.X509_CERTIFICATE) {
  76 + encryptDeviceId(deviceCredentials);
  77 + }
73 log.trace("Executing updateDeviceCredentials [{}]", deviceCredentials); 78 log.trace("Executing updateDeviceCredentials [{}]", deviceCredentials);
74 credentialsValidator.validate(deviceCredentials); 79 credentialsValidator.validate(deviceCredentials);
75 return getData(deviceCredentialsDao.save(deviceCredentials)); 80 return getData(deviceCredentialsDao.save(deviceCredentials));
76 } 81 }
77 82
  83 + private void encryptDeviceId(DeviceCredentials deviceCredentials) {
  84 + String sha3Hash = EncryptionUtil.getSha3Hash(deviceCredentials.getCredentialsId());
  85 + deviceCredentials.setCredentialsId(sha3Hash);
  86 + }
  87 +
78 @Override 88 @Override
79 public void deleteDeviceCredentials(DeviceCredentials deviceCredentials) { 89 public void deleteDeviceCredentials(DeviceCredentials deviceCredentials) {
80 log.trace("Executing deleteDeviceCredentials [{}]", deviceCredentials); 90 log.trace("Executing deleteDeviceCredentials [{}]", deviceCredentials);
@@ -121,6 +131,10 @@ public class DeviceCredentialsServiceImpl implements DeviceCredentialsService { @@ -121,6 +131,10 @@ public class DeviceCredentialsServiceImpl implements DeviceCredentialsService {
121 throw new DataValidationException("Incorrect access token length [" + deviceCredentials.getCredentialsId().length() + "]!"); 131 throw new DataValidationException("Incorrect access token length [" + deviceCredentials.getCredentialsId().length() + "]!");
122 } 132 }
123 break; 133 break;
  134 + case X509_CERTIFICATE:
  135 + if (deviceCredentials.getCredentialsId().length() == 0) {
  136 + throw new DataValidationException("X509 Certificate Cannot be empty!");
  137 + }
124 default: 138 default:
125 break; 139 break;
126 } 140 }
@@ -69,6 +69,7 @@ @@ -69,6 +69,7 @@
69 <surfire.version>2.19.1</surfire.version> 69 <surfire.version>2.19.1</surfire.version>
70 <jar-plugin.version>3.0.2</jar-plugin.version> 70 <jar-plugin.version>3.0.2</jar-plugin.version>
71 <springfox-swagger.version>2.6.1</springfox-swagger.version> 71 <springfox-swagger.version>2.6.1</springfox-swagger.version>
  72 + <bouncycastle.version>1.56</bouncycastle.version>
72 </properties> 73 </properties>
73 74
74 <modules> 75 <modules>
@@ -689,6 +690,16 @@ @@ -689,6 +690,16 @@
689 <artifactId>springfox-swagger2</artifactId> 690 <artifactId>springfox-swagger2</artifactId>
690 <version>${springfox-swagger.version}</version> 691 <version>${springfox-swagger.version}</version>
691 </dependency> 692 </dependency>
  693 + <dependency>
  694 + <groupId>org.bouncycastle</groupId>
  695 + <artifactId>bcprov-jdk15on</artifactId>
  696 + <version>${bouncycastle.version}</version>
  697 + </dependency>
  698 + <dependency>
  699 + <groupId>org.bouncycastle</groupId>
  700 + <artifactId>bcpkix-jdk15on</artifactId>
  701 + <version>${bouncycastle.version}</version>
  702 + </dependency>
692 </dependencies> 703 </dependencies>
693 </dependencyManagement> 704 </dependencyManagement>
694 705
1 HOSTNAME="$(hostname)" 1 HOSTNAME="$(hostname)"
2 PASSWORD="password" 2 PASSWORD="password"
3 3
4 -CLIENT_TRUSTSTORE="client_truststore.crt" 4 +CLIENT_TRUSTSTORE="client_truststore.pem"
  5 +CLIENT_KEY_ALIAS="clientalias"
  6 +CLIENT_FILE_PREFIX="mqttclient"
5 7
6 SERVER_KEY_ALIAS="serveralias" 8 SERVER_KEY_ALIAS="serveralias"
7 SERVER_FILE_PREFIX="mqttserver" 9 SERVER_FILE_PREFIX="mqttserver"
@@ -45,6 +45,7 @@ read -p "Do you want to copy $SERVER_FILE_PREFIX.jks to server directory? " yn @@ -45,6 +45,7 @@ read -p "Do you want to copy $SERVER_FILE_PREFIX.jks to server directory? " yn
45 else 45 else
46 DESTINATION=$SERVER_KEYSTORE_DIR 46 DESTINATION=$SERVER_KEYSTORE_DIR
47 fi; 47 fi;
  48 + mkdir -p $SERVER_KEYSTORE_DIR
48 cp $SERVER_FILE_PREFIX.jks $DESTINATION 49 cp $SERVER_FILE_PREFIX.jks $DESTINATION
49 if [ $? -ne 0 ]; then 50 if [ $? -ne 0 ]; then
50 echo "Failed to copy keystore file." 51 echo "Failed to copy keystore file."
  1 +# -*- coding: utf-8 -*-
  2 +#
  3 +# Copyright © 2016 The Thingsboard Authors
  4 +#
  5 +# Licensed under the Apache License, Version 2.0 (the "License");
  6 +# you may not use this file except in compliance with the License.
  7 +# You may obtain a copy of the License at
  8 +#
  9 +# http://www.apache.org/licenses/LICENSE-2.0
  10 +#
  11 +# Unless required by applicable law or agreed to in writing, software
  12 +# distributed under the License is distributed on an "AS IS" BASIS,
  13 +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 +# See the License for the specific language governing permissions and
  15 +# limitations under the License.
  16 +#
  17 +
  18 +import paho.mqtt.client as mqtt
  19 +import ssl, socket
  20 +
  21 +# The callback for when the client receives a CONNACK response from the server.
  22 +def on_connect(client, userdata, rc):
  23 + print('Connected with result code '+str(rc))
  24 + # Subscribing in on_connect() means that if we lose the connection and
  25 + # reconnect then subscriptions will be renewed.
  26 + client.subscribe('v1/devices/me/attributes')
  27 + client.subscribe('v1/devices/me/attributes/response/+')
  28 + client.subscribe('v1/devices/me/rpc/request/+')
  29 +
  30 +
  31 +# The callback for when a PUBLISH message is received from the server.
  32 +def on_message(client, userdata, msg):
  33 + print 'Topic: ' + msg.topic + '\nMessage: ' + str(msg.payload)
  34 + if msg.topic.startswith( 'v1/devices/me/rpc/request/'):
  35 + requestId = msg.topic[len('v1/devices/me/rpc/request/'):len(msg.topic)]
  36 + print 'This is a RPC call. RequestID: ' + requestId + '. Going to reply now!'
  37 + client.publish('v1/devices/me/rpc/response/' + requestId, "{\"value1\":\"A\", \"value2\":\"B\"}", 1)
  38 +
  39 +
  40 +client = mqtt.Client()
  41 +client.on_connect = on_connect
  42 +client.on_message = on_message
  43 +client.publish('v1/devices/me/attributes/request/1', "{\"clientKeys\":\"model\"}", 1)
  44 +
  45 +#client.tls_set(ca_certs="client_truststore.pem", certfile="mqttclient.nopass.pem", keyfile=None, cert_reqs=ssl.CERT_REQUIRED,
  46 +# tls_version=ssl.PROTOCOL_TLSv1, ciphers=None);
  47 +client.tls_set(ca_certs="client_truststore.pem", certfile=None, keyfile=None, cert_reqs=ssl.CERT_REQUIRED,
  48 + tls_version=ssl.PROTOCOL_TLSv1, ciphers=None);
  49 +
  50 +client.username_pw_set("B1_TEST_TOKEN")
  51 +client.tls_insecure_set(False)
  52 +client.connect(socket.gethostname(), 1883, 1)
  53 +
  54 +
  55 +# Blocking call that processes network traffic, dispatches callbacks and
  56 +# handles reconnecting.
  57 +# Other loop*() functions are available that give a threaded interface and a
  58 +# manual interface.
  59 +client.loop_forever()
  1 +#!/bin/sh
  2 +#
  3 +# Copyright © 2016 The Thingsboard Authors
  4 +#
  5 +# Licensed under the Apache License, Version 2.0 (the "License");
  6 +# you may not use this file except in compliance with the License.
  7 +# You may obtain a copy of the License at
  8 +#
  9 +# http://www.apache.org/licenses/LICENSE-2.0
  10 +#
  11 +# Unless required by applicable law or agreed to in writing, software
  12 +# distributed under the License is distributed on an "AS IS" BASIS,
  13 +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 +# See the License for the specific language governing permissions and
  15 +# limitations under the License.
  16 +#
  17 +
  18 +
  19 +. keygen.properties
  20 +
  21 +echo "Generating SSL Key Pair..."
  22 +
  23 +keytool -genkeypair -v \
  24 + -alias $CLIENT_KEY_ALIAS \
  25 + -dname "CN=$HOSTNAME, OU=Thingsboard, O=Thingsboard, L=Piscataway, ST=NJ, C=US" \
  26 + -keystore $CLIENT_FILE_PREFIX.jks \
  27 + -keypass $PASSWORD \
  28 + -storepass $PASSWORD \
  29 + -keyalg RSA \
  30 + -keysize 2048 \
  31 + -validity 9999
  32 +echo "Converting keystore to pkcs12"
  33 +keytool -importkeystore \
  34 + -srckeystore $CLIENT_FILE_PREFIX.jks \
  35 + -destkeystore $CLIENT_FILE_PREFIX.p12 \
  36 + -srcalias $CLIENT_KEY_ALIAS \
  37 + -srcstoretype jks \
  38 + -deststoretype pkcs12 \
  39 + -keypass $PASSWORD \
  40 + -srcstorepass $PASSWORD \
  41 + -deststorepass $PASSWORD \
  42 + -srckeypass $PASSWORD \
  43 + -destkeypass $PASSWORD
  44 +
  45 +echo "Converting pkcs12 to pem"
  46 +openssl pkcs12 -in $CLIENT_FILE_PREFIX.p12 \
  47 + -out $CLIENT_FILE_PREFIX.pem \
  48 + -passin pass:$PASSWORD \
  49 + -passout pass:$PASSWORD \
  50 +
  51 +echo "Importing server public key..."
  52 +keytool -export \
  53 + -alias $SERVER_KEY_ALIAS \
  54 + -keystore $SERVER_KEYSTORE_DIR/$SERVER_FILE_PREFIX.jks \
  55 + -file $CLIENT_TRUSTSTORE -rfc \
  56 + -storepass $PASSWORD
  57 +
  58 +echo "Exporting no-password pem certificate"
  59 +openssl rsa -in $CLIENT_FILE_PREFIX.pem -out $CLIENT_FILE_PREFIX.nopass.pem -passin pass:$PASSWORD
  60 +tail -n +$(($(grep -m1 -n -e '-----BEGIN CERTIFICATE' $CLIENT_FILE_PREFIX.pem | cut -d: -f1) )) \
  61 + $CLIENT_FILE_PREFIX.pem >> $CLIENT_FILE_PREFIX.nopass.pem
  62 +
  63 +echo "Done."
tools/src/main/shell/twowaysslmqttclient.py renamed from tools/src/main/shell/securemqttclient.py
  1 +# -*- coding: utf-8 -*-
1 # 2 #
2 # Copyright © 2016-2017 The Thingsboard Authors 3 # Copyright © 2016-2017 The Thingsboard Authors
3 # 4 #
@@ -41,8 +42,9 @@ client.on_connect = on_connect @@ -41,8 +42,9 @@ client.on_connect = on_connect
41 client.on_message = on_message 42 client.on_message = on_message
42 client.publish('v1/devices/me/attributes/request/1', "{\"clientKeys\":\"model\"}", 1) 43 client.publish('v1/devices/me/attributes/request/1', "{\"clientKeys\":\"model\"}", 1)
43 44
44 -client.tls_set(ca_certs="client_truststore.crt", certfile=None, keyfile=None, cert_reqs=ssl.CERT_REQUIRED,  
45 - tls_version=ssl.PROTOCOL_TLSv1, ciphers=None); 45 +client.tls_set(ca_certs="client_truststore.pem", certfile="mqttclient.nopass.pem", keyfile=None, cert_reqs=ssl.CERT_REQUIRED,
  46 + tls_version=ssl.PROTOCOL_TLSv1, ciphers=None);
  47 +
46 client.username_pw_set("TEST_TOKEN") 48 client.username_pw_set("TEST_TOKEN")
47 client.tls_insecure_set(False) 49 client.tls_insecure_set(False)
48 client.connect(socket.gethostname(), 1883, 1) 50 client.connect(socket.gethostname(), 1883, 1)
@@ -18,15 +18,23 @@ package org.thingsboard.server.transport.mqtt; @@ -18,15 +18,23 @@ package org.thingsboard.server.transport.mqtt;
18 import com.google.common.io.Resources; 18 import com.google.common.io.Resources;
19 import io.netty.handler.ssl.SslHandler; 19 import io.netty.handler.ssl.SslHandler;
20 import lombok.extern.slf4j.Slf4j; 20 import lombok.extern.slf4j.Slf4j;
  21 +import org.springframework.beans.factory.annotation.Autowired;
21 import org.springframework.beans.factory.annotation.Value; 22 import org.springframework.beans.factory.annotation.Value;
22 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 23 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
23 import org.springframework.stereotype.Component; 24 import org.springframework.stereotype.Component;
  25 +import org.thingsboard.server.common.data.security.DeviceCredentials;
  26 +import org.thingsboard.server.dao.EncryptionUtil;
  27 +import org.thingsboard.server.dao.device.DeviceCredentialsService;
  28 +import org.thingsboard.server.transport.mqtt.util.SslUtil;
24 29
25 import javax.net.ssl.*; 30 import javax.net.ssl.*;
26 import java.io.File; 31 import java.io.File;
27 import java.io.FileInputStream; 32 import java.io.FileInputStream;
  33 +import java.io.IOException;
28 import java.net.URL; 34 import java.net.URL;
29 import java.security.KeyStore; 35 import java.security.KeyStore;
  36 +import java.security.cert.CertificateException;
  37 +import java.security.cert.X509Certificate;
30 38
31 /** 39 /**
32 * Created by valerii.sosliuk on 11/6/16. 40 * Created by valerii.sosliuk on 11/6/16.
@@ -51,6 +59,9 @@ public class MqttSslHandlerProvider { @@ -51,6 +59,9 @@ public class MqttSslHandlerProvider {
51 @Value("${mqtt.ssl.trustStoreType}") 59 @Value("${mqtt.ssl.trustStoreType}")
52 private String trustStoreType; 60 private String trustStoreType;
53 61
  62 + @Autowired
  63 + private DeviceCredentialsService deviceCredentialsService;
  64 +
54 65
55 public SslHandler getSslHandler() { 66 public SslHandler getSslHandler() {
56 try { 67 try {
@@ -71,13 +82,14 @@ public class MqttSslHandlerProvider { @@ -71,13 +82,14 @@ public class MqttSslHandlerProvider {
71 kmf.init(ks, keyStorePassword.toCharArray()); 82 kmf.init(ks, keyStorePassword.toCharArray());
72 83
73 KeyManager[] km = kmf.getKeyManagers(); 84 KeyManager[] km = kmf.getKeyManagers();
74 - TrustManager[] tm = tmFactory.getTrustManagers(); 85 + TrustManager x509wrapped = getX509TrustManager(tmFactory);
  86 + TrustManager[] tm = {x509wrapped};
75 SSLContext sslContext = SSLContext.getInstance(TLS); 87 SSLContext sslContext = SSLContext.getInstance(TLS);
76 sslContext.init(km, tm, null); 88 sslContext.init(km, tm, null);
77 SSLEngine sslEngine = sslContext.createSSLEngine(); 89 SSLEngine sslEngine = sslContext.createSSLEngine();
78 sslEngine.setUseClientMode(false); 90 sslEngine.setUseClientMode(false);
79 sslEngine.setNeedClientAuth(false); 91 sslEngine.setNeedClientAuth(false);
80 - sslEngine.setWantClientAuth(false); 92 + sslEngine.setWantClientAuth(true);
81 sslEngine.setEnabledProtocols(sslEngine.getSupportedProtocols()); 93 sslEngine.setEnabledProtocols(sslEngine.getSupportedProtocols());
82 sslEngine.setEnabledCipherSuites(sslEngine.getSupportedCipherSuites()); 94 sslEngine.setEnabledCipherSuites(sslEngine.getSupportedCipherSuites());
83 sslEngine.setEnableSessionCreation(true); 95 sslEngine.setEnableSessionCreation(true);
@@ -88,4 +100,54 @@ public class MqttSslHandlerProvider { @@ -88,4 +100,54 @@ public class MqttSslHandlerProvider {
88 } 100 }
89 } 101 }
90 102
  103 + private TrustManager getX509TrustManager(TrustManagerFactory tmf) throws Exception {
  104 + X509TrustManager x509Tm = null;
  105 + for (TrustManager tm : tmf.getTrustManagers()) {
  106 + if (tm instanceof X509TrustManager) {
  107 + x509Tm = (X509TrustManager) tm;
  108 + break;
  109 + }
  110 + }
  111 + X509TrustManager x509TmWrapper = new ThingsboardMqttX509TrustManager(x509Tm, deviceCredentialsService);
  112 + return x509TmWrapper;
  113 + }
  114 +
  115 + static class ThingsboardMqttX509TrustManager implements X509TrustManager {
  116 +
  117 + private final X509TrustManager trustManager;
  118 + private DeviceCredentialsService deviceCredentialsService;
  119 +
  120 + ThingsboardMqttX509TrustManager(X509TrustManager trustManager, DeviceCredentialsService deviceCredentialsService) {
  121 + this.trustManager = trustManager;
  122 + this.deviceCredentialsService = deviceCredentialsService;
  123 + }
  124 +
  125 + @Override
  126 + public X509Certificate[] getAcceptedIssuers() {
  127 + return trustManager.getAcceptedIssuers();
  128 + }
  129 +
  130 + @Override
  131 + public void checkServerTrusted(X509Certificate[] chain,
  132 + String authType) throws CertificateException {
  133 + trustManager.checkServerTrusted(chain, authType);
  134 + }
  135 +
  136 + @Override
  137 + public void checkClientTrusted(X509Certificate[] chain,
  138 + String authType) throws CertificateException {
  139 + for (X509Certificate cert : chain) {
  140 + try {
  141 + String strCert = SslUtil.getX509CertificateString(cert);
  142 + String sha3Hash = EncryptionUtil.getSha3Hash(strCert);
  143 + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(sha3Hash);
  144 + if (deviceCredentials == null) {
  145 + throw new CertificateException("Invalid Device Certificate");
  146 + }
  147 + } catch (IOException e) {
  148 + e.printStackTrace();
  149 + }
  150 + }
  151 + }
  152 + }
91 } 153 }
@@ -18,11 +18,13 @@ package org.thingsboard.server.transport.mqtt; @@ -18,11 +18,13 @@ package org.thingsboard.server.transport.mqtt;
18 import io.netty.channel.ChannelHandlerContext; 18 import io.netty.channel.ChannelHandlerContext;
19 import io.netty.channel.ChannelInboundHandlerAdapter; 19 import io.netty.channel.ChannelInboundHandlerAdapter;
20 import io.netty.handler.codec.mqtt.*; 20 import io.netty.handler.codec.mqtt.*;
  21 +import io.netty.handler.ssl.SslHandler;
21 import io.netty.util.concurrent.Future; 22 import io.netty.util.concurrent.Future;
22 import io.netty.util.concurrent.GenericFutureListener; 23 import io.netty.util.concurrent.GenericFutureListener;
23 import lombok.extern.slf4j.Slf4j; 24 import lombok.extern.slf4j.Slf4j;
24 import org.springframework.util.StringUtils; 25 import org.springframework.util.StringUtils;
25 import org.thingsboard.server.common.data.security.DeviceTokenCredentials; 26 import org.thingsboard.server.common.data.security.DeviceTokenCredentials;
  27 +import org.thingsboard.server.common.data.security.DeviceX509Credentials;
26 import org.thingsboard.server.common.msg.session.AdaptorToSessionActorMsg; 28 import org.thingsboard.server.common.msg.session.AdaptorToSessionActorMsg;
27 import org.thingsboard.server.common.msg.session.BasicToDeviceActorSessionMsg; 29 import org.thingsboard.server.common.msg.session.BasicToDeviceActorSessionMsg;
28 import org.thingsboard.server.common.msg.session.MsgType; 30 import org.thingsboard.server.common.msg.session.MsgType;
@@ -30,9 +32,13 @@ import org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg; @@ -30,9 +32,13 @@ import org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg;
30 import org.thingsboard.server.common.transport.SessionMsgProcessor; 32 import org.thingsboard.server.common.transport.SessionMsgProcessor;
31 import org.thingsboard.server.common.transport.adaptor.AdaptorException; 33 import org.thingsboard.server.common.transport.adaptor.AdaptorException;
32 import org.thingsboard.server.common.transport.auth.DeviceAuthService; 34 import org.thingsboard.server.common.transport.auth.DeviceAuthService;
  35 +import org.thingsboard.server.dao.EncryptionUtil;
33 import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; 36 import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor;
34 import org.thingsboard.server.transport.mqtt.session.MqttSessionCtx; 37 import org.thingsboard.server.transport.mqtt.session.MqttSessionCtx;
  38 +import org.thingsboard.server.transport.mqtt.util.SslUtil;
35 39
  40 +import javax.net.ssl.SSLPeerUnverifiedException;
  41 +import javax.security.cert.X509Certificate;
36 import java.util.ArrayList; 42 import java.util.ArrayList;
37 import java.util.List; 43 import java.util.List;
38 44
@@ -57,12 +63,15 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @@ -57,12 +63,15 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
57 private final String sessionId; 63 private final String sessionId;
58 private final MqttTransportAdaptor adaptor; 64 private final MqttTransportAdaptor adaptor;
59 private final SessionMsgProcessor processor; 65 private final SessionMsgProcessor processor;
  66 + private final SslHandler sslHandler;
60 67
61 - public MqttTransportHandler(SessionMsgProcessor processor, DeviceAuthService authService, MqttTransportAdaptor adaptor) { 68 + public MqttTransportHandler(SessionMsgProcessor processor, DeviceAuthService authService,
  69 + MqttTransportAdaptor adaptor, SslHandler sslHandler) {
62 this.processor = processor; 70 this.processor = processor;
63 this.adaptor = adaptor; 71 this.adaptor = adaptor;
64 this.sessionCtx = new MqttSessionCtx(processor, authService, adaptor); 72 this.sessionCtx = new MqttSessionCtx(processor, authService, adaptor);
65 this.sessionId = sessionCtx.getSessionId().toUidStr(); 73 this.sessionId = sessionCtx.getSessionId().toUidStr();
  74 + this.sslHandler = sslHandler;
66 } 75 }
67 76
68 @Override 77 @Override
@@ -197,6 +206,15 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @@ -197,6 +206,15 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
197 206
198 private void processConnect(ChannelHandlerContext ctx, MqttConnectMessage msg) { 207 private void processConnect(ChannelHandlerContext ctx, MqttConnectMessage msg) {
199 log.info("[{}] Processing connect msg for client: {}!", sessionId, msg.payload().clientIdentifier()); 208 log.info("[{}] Processing connect msg for client: {}!", sessionId, msg.payload().clientIdentifier());
  209 + X509Certificate cert;
  210 + if (sslHandler != null && (cert = getX509Certificate()) != null) {
  211 + processX509CertConnect(ctx, cert);
  212 + } else {
  213 + processAuthTokenConnect(ctx, msg);
  214 + }
  215 + }
  216 +
  217 + private void processAuthTokenConnect(ChannelHandlerContext ctx, MqttConnectMessage msg) {
200 String userName = msg.payload().userName(); 218 String userName = msg.payload().userName();
201 if (StringUtils.isEmpty(userName)) { 219 if (StringUtils.isEmpty(userName)) {
202 ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD)); 220 ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD));
@@ -209,6 +227,35 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @@ -209,6 +227,35 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
209 } 227 }
210 } 228 }
211 229
  230 + private void processX509CertConnect(ChannelHandlerContext ctx, X509Certificate cert) {
  231 + try {
  232 + String strCert = SslUtil.getX509CertificateString(cert);
  233 + String sha3Hash = EncryptionUtil.getSha3Hash(strCert);
  234 + if (sessionCtx.login(new DeviceX509Credentials(sha3Hash))) {
  235 + ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_ACCEPTED));
  236 + } else {
  237 + ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_NOT_AUTHORIZED));
  238 + ctx.close();
  239 + }
  240 + } catch (Exception e) {
  241 + ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_NOT_AUTHORIZED));
  242 + ctx.close();
  243 + }
  244 + }
  245 +
  246 + private X509Certificate getX509Certificate() {
  247 + try {
  248 + X509Certificate[] certChain = sslHandler.engine().getSession().getPeerCertificateChain();
  249 + if (certChain.length > 0) {
  250 + return certChain[0];
  251 + }
  252 + } catch (SSLPeerUnverifiedException e) {
  253 + log.warn(e.getMessage());
  254 + return null;
  255 + }
  256 + return null;
  257 + }
  258 +
212 private void processDisconnect(ChannelHandlerContext ctx) { 259 private void processDisconnect(ChannelHandlerContext ctx) {
213 ctx.close(); 260 ctx.close();
214 } 261 }
@@ -55,13 +55,15 @@ public class MqttTransportServerInitializer extends ChannelInitializer<SocketCha @@ -55,13 +55,15 @@ public class MqttTransportServerInitializer extends ChannelInitializer<SocketCha
55 @Override 55 @Override
56 public void initChannel(SocketChannel ch) { 56 public void initChannel(SocketChannel ch) {
57 ChannelPipeline pipeline = ch.pipeline(); 57 ChannelPipeline pipeline = ch.pipeline();
  58 + SslHandler sslHandler = null;
58 if (sslHandlerProvider != null) { 59 if (sslHandlerProvider != null) {
59 - pipeline.addLast(sslHandlerProvider.getSslHandler()); 60 + sslHandler = sslHandlerProvider.getSslHandler();
  61 + pipeline.addLast(sslHandler);
60 } 62 }
61 pipeline.addLast("decoder", new MqttDecoder()); 63 pipeline.addLast("decoder", new MqttDecoder());
62 pipeline.addLast("encoder", MqttEncoder.INSTANCE); 64 pipeline.addLast("encoder", MqttEncoder.INSTANCE);
63 65
64 - MqttTransportHandler handler = new MqttTransportHandler(processor, authService, adaptor); 66 + MqttTransportHandler handler = new MqttTransportHandler(processor, authService, adaptor, sslHandler);
65 pipeline.addLast(handler); 67 pipeline.addLast(handler);
66 ch.closeFuture().addListener(handler); 68 ch.closeFuture().addListener(handler);
67 } 69 }
  1 +/**
  2 + * Copyright © 2016 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.transport.mqtt.util;
  17 +
  18 +import lombok.extern.slf4j.Slf4j;
  19 +import sun.misc.BASE64Encoder;
  20 +
  21 +import java.io.ByteArrayOutputStream;
  22 +import java.io.IOException;
  23 +import java.security.cert.CertificateEncodingException;
  24 +import java.security.cert.X509Certificate;
  25 +
  26 +/**
  27 + * @author Valerii Sosliuk
  28 + */
  29 +@Slf4j
  30 +public class SslUtil {
  31 +
  32 + private SslUtil() {
  33 + }
  34 +
  35 + public static String getX509CertificateString(X509Certificate cert) throws CertificateEncodingException, IOException {
  36 + ByteArrayOutputStream out = new ByteArrayOutputStream();
  37 + BASE64Encoder encoder = new BASE64Encoder();
  38 + encoder.encodeBuffer(cert.getEncoded(), out);
  39 + return new String(out.toByteArray(), "UTF-8").trim();
  40 + }
  41 +
  42 + public static String getX509CertificateString(javax.security.cert.X509Certificate cert)
  43 + throws javax.security.cert.CertificateEncodingException, IOException {
  44 + ByteArrayOutputStream out = new ByteArrayOutputStream();
  45 + BASE64Encoder encoder = new BASE64Encoder();
  46 + encoder.encodeBuffer(cert.getEncoded(), out);
  47 + return new String(out.toByteArray(), "UTF-8").trim();
  48 + }
  49 +}
@@ -42,9 +42,17 @@ @@ -42,9 +42,17 @@
42 42
43 <dependencies> 43 <dependencies>
44 <dependency> 44 <dependency>
  45 + <groupId>org.thingsboard</groupId>
  46 + <artifactId>dao</artifactId>
  47 + </dependency>
  48 + <dependency>
45 <groupId>org.springframework.boot</groupId> 49 <groupId>org.springframework.boot</groupId>
46 <artifactId>spring-boot-autoconfigure</artifactId> 50 <artifactId>spring-boot-autoconfigure</artifactId>
47 </dependency> 51 </dependency>
  52 + <dependency>
  53 + <groupId>org.bouncycastle</groupId>
  54 + <artifactId>bcprov-jdk15on</artifactId>
  55 + </dependency>
48 </dependencies> 56 </dependencies>
49 57
50 </project> 58 </project>
@@ -24,7 +24,7 @@ export default function ManageDeviceCredentialsController(deviceService, $scope, @@ -24,7 +24,7 @@ export default function ManageDeviceCredentialsController(deviceService, $scope,
24 value: 'ACCESS_TOKEN' 24 value: 'ACCESS_TOKEN'
25 }, 25 },
26 { 26 {
27 - name: 'X.509 Certificate (Coming soon)', 27 + name: 'X.509 Certificate',
28 value: 'X509_CERTIFICATE' 28 value: 'X509_CERTIFICATE'
29 } 29 }
30 ]; 30 ];
@@ -35,6 +35,7 @@ export default function ManageDeviceCredentialsController(deviceService, $scope, @@ -35,6 +35,7 @@ export default function ManageDeviceCredentialsController(deviceService, $scope,
35 vm.valid = valid; 35 vm.valid = valid;
36 vm.cancel = cancel; 36 vm.cancel = cancel;
37 vm.save = save; 37 vm.save = save;
  38 + vm.clear = clear;
38 39
39 loadDeviceCredentials(); 40 loadDeviceCredentials();
40 41
@@ -50,10 +51,16 @@ export default function ManageDeviceCredentialsController(deviceService, $scope, @@ -50,10 +51,16 @@ export default function ManageDeviceCredentialsController(deviceService, $scope,
50 51
51 function valid() { 52 function valid() {
52 return vm.deviceCredentials && 53 return vm.deviceCredentials &&
53 - vm.deviceCredentials.credentialsType === 'ACCESS_TOKEN' && 54 + (vm.deviceCredentials.credentialsType === 'ACCESS_TOKEN'
  55 + || vm.deviceCredentials.credentialsType === 'X509_CERTIFICATE')
  56 + &&
54 vm.deviceCredentials.credentialsId && vm.deviceCredentials.credentialsId.length > 0; 57 vm.deviceCredentials.credentialsId && vm.deviceCredentials.credentialsId.length > 0;
55 } 58 }
56 59
  60 + function clear() {
  61 + vm.deviceCredentials.credentialsId = null;
  62 + }
  63 +
57 function save() { 64 function save() {
58 deviceService.saveDeviceCredentials(vm.deviceCredentials).then(function success(deviceCredentials) { 65 deviceService.saveDeviceCredentials(vm.deviceCredentials).then(function success(deviceCredentials) {
59 vm.deviceCredentials = deviceCredentials; 66 vm.deviceCredentials = deviceCredentials;
@@ -33,7 +33,8 @@ @@ -33,7 +33,8 @@
33 <fieldset ng-disabled="loading || vm.isReadOnly"> 33 <fieldset ng-disabled="loading || vm.isReadOnly">
34 <md-input-container class="md-block"> 34 <md-input-container class="md-block">
35 <label translate>device.credentials-type</label> 35 <label translate>device.credentials-type</label>
36 - <md-select ng-disabled="loading || vm.isReadOnly" ng-model="vm.deviceCredentials.credentialsType"> 36 + <md-select ng-disabled="loading || vm.isReadOnly" ng-model="vm.deviceCredentials.credentialsType"
  37 + ng-change="vm.clear()">
37 <md-option ng-repeat="credentialsType in vm.credentialsTypes" value="{{credentialsType.value}}"> 38 <md-option ng-repeat="credentialsType in vm.credentialsTypes" value="{{credentialsType.value}}">
38 {{credentialsType.name}} 39 {{credentialsType.name}}
39 </md-option> 40 </md-option>
@@ -48,6 +49,14 @@ @@ -48,6 +49,14 @@
48 <div translate ng-message="pattern">device.access-token-invalid</div> 49 <div translate ng-message="pattern">device.access-token-invalid</div>
49 </div> 50 </div>
50 </md-input-container> 51 </md-input-container>
  52 + <md-input-container class="md-block" ng-if="vm.deviceCredentials.credentialsType === 'X509_CERTIFICATE'">
  53 + <label translate>device.rsa-key</label>
  54 + <textarea required name="rsaKey" ng-model="vm.deviceCredentials.credentialsId"
  55 + cols="15" rows="5" />
  56 + <div ng-messages="theForm.rsaKey.$error">
  57 + <div translate ng-message="required">device.rsa-key-required</div>
  58 + </div>
  59 + </md-input-container>
51 </fieldset> 60 </fieldset>
52 </div> 61 </div>
53 </md-dialog-content> 62 </md-dialog-content>
@@ -308,6 +308,8 @@ @@ -308,6 +308,8 @@
308 "access-token": "Access token", 308 "access-token": "Access token",
309 "access-token-required": "Access token is required.", 309 "access-token-required": "Access token is required.",
310 "access-token-invalid": "Access token length must be from 1 to 20 characters.", 310 "access-token-invalid": "Access token length must be from 1 to 20 characters.",
  311 + "rsa-key": "RSA public key",
  312 + "access-token-required": "RSA public key is required.",
311 "secret": "Secret", 313 "secret": "Secret",
312 "secret-required": "Secret is required.", 314 "secret-required": "Secret is required.",
313 "name": "Name", 315 "name": "Name",