From 75f39693ffcbb2a814d3b4262b2d4faf64b416a2 Mon Sep 17 00:00:00 2001 From: Valerii Sosliuk Date: Sun, 8 Jan 2017 20:16:52 -0500 Subject: [PATCH] X509 certificate support implemented --- .../device/DefaultDeviceAuthService.java | 2 + .../data/security/DeviceCredentialsType.java | 3 +- .../data/security/DeviceTokenCredentials.java | 1 - .../data/security/DeviceX509Credentials.java | 39 +++++++++++ dao/pom.xml | 4 ++ .../org/thingsboard/server/dao/DaoUtil.java | 2 - .../server/dao/EncryptionUtil.java | 41 ++++++++++++ .../device/DeviceCredentialsServiceImpl.java | 14 ++++ pom.xml | 11 ++++ tools/src/main/shell/keygen.properties | 4 +- tools/src/main/shell/keygen.sh | 1 + tools/src/main/shell/onewaysslmqttclient.py | 59 +++++++++++++++++ .../src/main/shell/securemqttclient.keygen.sh | 63 ++++++++++++++++++ ...remqttclient.py => twowaysslmqttclient.py} | 6 +- .../mqtt/MqttSslHandlerProvider.java | 66 ++++++++++++++++++- .../transport/mqtt/MqttTransportHandler.java | 49 +++++++++++++- .../mqtt/MqttTransportServerInitializer.java | 6 +- .../server/transport/mqtt/util/SslUtil.java | 49 ++++++++++++++ transport/pom.xml | 8 +++ .../device/device-credentials.controller.js | 11 +++- ui/src/app/device/device-credentials.tpl.html | 11 +++- ui/src/locale/en_US.json | 2 + 22 files changed, 437 insertions(+), 15 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceX509Credentials.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/EncryptionUtil.java create mode 100644 tools/src/main/shell/onewaysslmqttclient.py create mode 100755 tools/src/main/shell/securemqttclient.keygen.sh rename tools/src/main/shell/{securemqttclient.py => twowaysslmqttclient.py} (90%) create mode 100644 transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/SslUtil.java diff --git a/application/src/main/java/org/thingsboard/server/service/security/device/DefaultDeviceAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/device/DefaultDeviceAuthService.java index cef73fe111..dbc1920414 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/device/DefaultDeviceAuthService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/device/DefaultDeviceAuthService.java @@ -51,6 +51,8 @@ public class DefaultDeviceAuthService implements DeviceAuthService { // Credentials ID matches Credentials value in this // primitive case; return DeviceAuthResult.of(credentials.getDeviceId()); + case X509_CERTIFICATE: + return DeviceAuthResult.of(credentials.getDeviceId()); default: return DeviceAuthResult.of("Credentials Type is not supported yet!"); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsType.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsType.java index 3daa1e4e1b..d388348775 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsType.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.security; public enum DeviceCredentialsType { - ACCESS_TOKEN + ACCESS_TOKEN, + X509_CERTIFICATE } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceTokenCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceTokenCredentials.java index 8ce9f00484..3e9001bdd9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceTokenCredentials.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceTokenCredentials.java @@ -20,7 +20,6 @@ public class DeviceTokenCredentials implements DeviceCredentialsFilter { private final String token; public DeviceTokenCredentials(String token) { - super(); this.token = token; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceX509Credentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceX509Credentials.java new file mode 100644 index 0000000000..f2be4c1907 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceX509Credentials.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.security; + +/** + * @author Valerii Sosliuk + */ +public class DeviceX509Credentials implements DeviceCredentialsFilter { + + private final String sha3Hash; + + public DeviceX509Credentials(String sha3Hash) { + this.sha3Hash = sha3Hash; + } + + @Override + public String getCredentialsId() { return sha3Hash; } + + @Override + public DeviceCredentialsType getCredentialsType() { return DeviceCredentialsType.X509_CERTIFICATE; } + + @Override + public String toString() { + return "DeviceX509Credentials [SHA3=" + sha3Hash + "]"; + } +} diff --git a/dao/pom.xml b/dao/pom.xml index a2edc7a5e4..3b25c428b1 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -146,6 +146,10 @@ org.springframework.boot spring-boot-autoconfigure + + org.bouncycastle + bcprov-jdk15on + diff --git a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java index da1a42f9de..f258122bae 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java @@ -18,9 +18,7 @@ package org.thingsboard.server.dao; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashSet; import java.util.List; -import java.util.Set; import java.util.UUID; import org.thingsboard.server.common.data.id.UUIDBased; diff --git a/dao/src/main/java/org/thingsboard/server/dao/EncryptionUtil.java b/dao/src/main/java/org/thingsboard/server/dao/EncryptionUtil.java new file mode 100644 index 0000000000..df456c8714 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/EncryptionUtil.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao; + +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.crypto.digests.SHA3Digest; +import org.bouncycastle.pqc.math.linearalgebra.ByteUtils; +/** + * @author Valerii Sosliuk + */ +@Slf4j +public class EncryptionUtil { + + private EncryptionUtil() { + } + + public static String getSha3Hash(String data) { + String trimmedData = data.replaceAll("\n","").replaceAll("\r",""); + byte[] dataBytes = trimmedData.getBytes(); + SHA3Digest md = new SHA3Digest(256); + md.reset(); + md.update(dataBytes, 0, dataBytes.length); + byte[] hashedBytes = new byte[256 / 8]; + md.doFinal(hashedBytes, 0); + String sha3Hash = ByteUtils.toHexString(hashedBytes); + return sha3Hash; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java index 19ad2d1114..cc0e642bd7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java @@ -23,6 +23,8 @@ import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.dao.EncryptionUtil; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.model.DeviceCredentialsEntity; import org.thingsboard.server.dao.service.DataValidator; @@ -70,11 +72,19 @@ public class DeviceCredentialsServiceImpl implements DeviceCredentialsService { } private DeviceCredentials saveOrUpdare(DeviceCredentials deviceCredentials) { + if (deviceCredentials.getCredentialsType() == DeviceCredentialsType.X509_CERTIFICATE) { + encryptDeviceId(deviceCredentials); + } log.trace("Executing updateDeviceCredentials [{}]", deviceCredentials); credentialsValidator.validate(deviceCredentials); return getData(deviceCredentialsDao.save(deviceCredentials)); } + private void encryptDeviceId(DeviceCredentials deviceCredentials) { + String sha3Hash = EncryptionUtil.getSha3Hash(deviceCredentials.getCredentialsId()); + deviceCredentials.setCredentialsId(sha3Hash); + } + @Override public void deleteDeviceCredentials(DeviceCredentials deviceCredentials) { log.trace("Executing deleteDeviceCredentials [{}]", deviceCredentials); @@ -121,6 +131,10 @@ public class DeviceCredentialsServiceImpl implements DeviceCredentialsService { throw new DataValidationException("Incorrect access token length [" + deviceCredentials.getCredentialsId().length() + "]!"); } break; + case X509_CERTIFICATE: + if (deviceCredentials.getCredentialsId().length() == 0) { + throw new DataValidationException("X509 Certificate Cannot be empty!"); + } default: break; } diff --git a/pom.xml b/pom.xml index b68bee7491..226f915d57 100755 --- a/pom.xml +++ b/pom.xml @@ -69,6 +69,7 @@ 2.19.1 3.0.2 2.6.1 + 1.56 @@ -689,6 +690,16 @@ springfox-swagger2 ${springfox-swagger.version} + + org.bouncycastle + bcprov-jdk15on + ${bouncycastle.version} + + + org.bouncycastle + bcpkix-jdk15on + ${bouncycastle.version} + diff --git a/tools/src/main/shell/keygen.properties b/tools/src/main/shell/keygen.properties index 6016bdae50..d27e0f5106 100644 --- a/tools/src/main/shell/keygen.properties +++ b/tools/src/main/shell/keygen.properties @@ -1,7 +1,9 @@ HOSTNAME="$(hostname)" PASSWORD="password" -CLIENT_TRUSTSTORE="client_truststore.crt" +CLIENT_TRUSTSTORE="client_truststore.pem" +CLIENT_KEY_ALIAS="clientalias" +CLIENT_FILE_PREFIX="mqttclient" SERVER_KEY_ALIAS="serveralias" SERVER_FILE_PREFIX="mqttserver" diff --git a/tools/src/main/shell/keygen.sh b/tools/src/main/shell/keygen.sh index 25b3157013..46e670d860 100755 --- a/tools/src/main/shell/keygen.sh +++ b/tools/src/main/shell/keygen.sh @@ -45,6 +45,7 @@ read -p "Do you want to copy $SERVER_FILE_PREFIX.jks to server directory? " yn else DESTINATION=$SERVER_KEYSTORE_DIR fi; + mkdir -p $SERVER_KEYSTORE_DIR cp $SERVER_FILE_PREFIX.jks $DESTINATION if [ $? -ne 0 ]; then echo "Failed to copy keystore file." diff --git a/tools/src/main/shell/onewaysslmqttclient.py b/tools/src/main/shell/onewaysslmqttclient.py new file mode 100644 index 0000000000..a6efbd35e2 --- /dev/null +++ b/tools/src/main/shell/onewaysslmqttclient.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# +# Copyright © 2016 The Thingsboard Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import paho.mqtt.client as mqtt +import ssl, socket + +# The callback for when the client receives a CONNACK response from the server. +def on_connect(client, userdata, rc): + print('Connected with result code '+str(rc)) + # Subscribing in on_connect() means that if we lose the connection and + # reconnect then subscriptions will be renewed. + client.subscribe('v1/devices/me/attributes') + client.subscribe('v1/devices/me/attributes/response/+') + client.subscribe('v1/devices/me/rpc/request/+') + + +# The callback for when a PUBLISH message is received from the server. +def on_message(client, userdata, msg): + print 'Topic: ' + msg.topic + '\nMessage: ' + str(msg.payload) + if msg.topic.startswith( 'v1/devices/me/rpc/request/'): + requestId = msg.topic[len('v1/devices/me/rpc/request/'):len(msg.topic)] + print 'This is a RPC call. RequestID: ' + requestId + '. Going to reply now!' + client.publish('v1/devices/me/rpc/response/' + requestId, "{\"value1\":\"A\", \"value2\":\"B\"}", 1) + + +client = mqtt.Client() +client.on_connect = on_connect +client.on_message = on_message +client.publish('v1/devices/me/attributes/request/1', "{\"clientKeys\":\"model\"}", 1) + +#client.tls_set(ca_certs="client_truststore.pem", certfile="mqttclient.nopass.pem", keyfile=None, cert_reqs=ssl.CERT_REQUIRED, +# tls_version=ssl.PROTOCOL_TLSv1, ciphers=None); +client.tls_set(ca_certs="client_truststore.pem", certfile=None, keyfile=None, cert_reqs=ssl.CERT_REQUIRED, + tls_version=ssl.PROTOCOL_TLSv1, ciphers=None); + +client.username_pw_set("B1_TEST_TOKEN") +client.tls_insecure_set(False) +client.connect(socket.gethostname(), 1883, 1) + + +# Blocking call that processes network traffic, dispatches callbacks and +# handles reconnecting. +# Other loop*() functions are available that give a threaded interface and a +# manual interface. +client.loop_forever() diff --git a/tools/src/main/shell/securemqttclient.keygen.sh b/tools/src/main/shell/securemqttclient.keygen.sh new file mode 100755 index 0000000000..1d6752c669 --- /dev/null +++ b/tools/src/main/shell/securemqttclient.keygen.sh @@ -0,0 +1,63 @@ +#!/bin/sh +# +# Copyright © 2016 The Thingsboard Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +. keygen.properties + +echo "Generating SSL Key Pair..." + +keytool -genkeypair -v \ + -alias $CLIENT_KEY_ALIAS \ + -dname "CN=$HOSTNAME, OU=Thingsboard, O=Thingsboard, L=Piscataway, ST=NJ, C=US" \ + -keystore $CLIENT_FILE_PREFIX.jks \ + -keypass $PASSWORD \ + -storepass $PASSWORD \ + -keyalg RSA \ + -keysize 2048 \ + -validity 9999 +echo "Converting keystore to pkcs12" +keytool -importkeystore \ + -srckeystore $CLIENT_FILE_PREFIX.jks \ + -destkeystore $CLIENT_FILE_PREFIX.p12 \ + -srcalias $CLIENT_KEY_ALIAS \ + -srcstoretype jks \ + -deststoretype pkcs12 \ + -keypass $PASSWORD \ + -srcstorepass $PASSWORD \ + -deststorepass $PASSWORD \ + -srckeypass $PASSWORD \ + -destkeypass $PASSWORD + +echo "Converting pkcs12 to pem" +openssl pkcs12 -in $CLIENT_FILE_PREFIX.p12 \ + -out $CLIENT_FILE_PREFIX.pem \ + -passin pass:$PASSWORD \ + -passout pass:$PASSWORD \ + +echo "Importing server public key..." +keytool -export \ + -alias $SERVER_KEY_ALIAS \ + -keystore $SERVER_KEYSTORE_DIR/$SERVER_FILE_PREFIX.jks \ + -file $CLIENT_TRUSTSTORE -rfc \ + -storepass $PASSWORD + +echo "Exporting no-password pem certificate" +openssl rsa -in $CLIENT_FILE_PREFIX.pem -out $CLIENT_FILE_PREFIX.nopass.pem -passin pass:$PASSWORD +tail -n +$(($(grep -m1 -n -e '-----BEGIN CERTIFICATE' $CLIENT_FILE_PREFIX.pem | cut -d: -f1) )) \ + $CLIENT_FILE_PREFIX.pem >> $CLIENT_FILE_PREFIX.nopass.pem + +echo "Done." \ No newline at end of file diff --git a/tools/src/main/shell/securemqttclient.py b/tools/src/main/shell/twowaysslmqttclient.py similarity index 90% rename from tools/src/main/shell/securemqttclient.py rename to tools/src/main/shell/twowaysslmqttclient.py index a3fbf17be8..b9c826debb 100644 --- a/tools/src/main/shell/securemqttclient.py +++ b/tools/src/main/shell/twowaysslmqttclient.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # # Copyright © 2016 The Thingsboard Authors # @@ -41,8 +42,9 @@ client.on_connect = on_connect client.on_message = on_message client.publish('v1/devices/me/attributes/request/1', "{\"clientKeys\":\"model\"}", 1) -client.tls_set(ca_certs="client_truststore.crt", certfile=None, keyfile=None, cert_reqs=ssl.CERT_REQUIRED, - tls_version=ssl.PROTOCOL_TLSv1, ciphers=None); +client.tls_set(ca_certs="client_truststore.pem", certfile="mqttclient.nopass.pem", keyfile=None, cert_reqs=ssl.CERT_REQUIRED, + tls_version=ssl.PROTOCOL_TLSv1, ciphers=None); + client.username_pw_set("TEST_TOKEN") client.tls_insecure_set(False) client.connect(socket.gethostname(), 1883, 1) diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java index f7b38d0f57..6872c03282 100644 --- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java +++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java @@ -18,15 +18,23 @@ package org.thingsboard.server.transport.mqtt; import com.google.common.io.Resources; import io.netty.handler.ssl.SslHandler; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.dao.EncryptionUtil; +import org.thingsboard.server.dao.device.DeviceCredentialsService; +import org.thingsboard.server.transport.mqtt.util.SslUtil; import javax.net.ssl.*; import java.io.File; import java.io.FileInputStream; +import java.io.IOException; import java.net.URL; import java.security.KeyStore; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; /** * Created by valerii.sosliuk on 11/6/16. @@ -51,6 +59,9 @@ public class MqttSslHandlerProvider { @Value("${mqtt.ssl.trustStoreType}") private String trustStoreType; + @Autowired + private DeviceCredentialsService deviceCredentialsService; + public SslHandler getSslHandler() { try { @@ -71,13 +82,14 @@ public class MqttSslHandlerProvider { kmf.init(ks, keyStorePassword.toCharArray()); KeyManager[] km = kmf.getKeyManagers(); - TrustManager[] tm = tmFactory.getTrustManagers(); + TrustManager x509wrapped = getX509TrustManager(tmFactory); + TrustManager[] tm = {x509wrapped}; SSLContext sslContext = SSLContext.getInstance(TLS); sslContext.init(km, tm, null); SSLEngine sslEngine = sslContext.createSSLEngine(); sslEngine.setUseClientMode(false); sslEngine.setNeedClientAuth(false); - sslEngine.setWantClientAuth(false); + sslEngine.setWantClientAuth(true); sslEngine.setEnabledProtocols(sslEngine.getSupportedProtocols()); sslEngine.setEnabledCipherSuites(sslEngine.getSupportedCipherSuites()); sslEngine.setEnableSessionCreation(true); @@ -88,4 +100,54 @@ public class MqttSslHandlerProvider { } } + private TrustManager getX509TrustManager(TrustManagerFactory tmf) throws Exception { + X509TrustManager x509Tm = null; + for (TrustManager tm : tmf.getTrustManagers()) { + if (tm instanceof X509TrustManager) { + x509Tm = (X509TrustManager) tm; + break; + } + } + X509TrustManager x509TmWrapper = new ThingsboardMqttX509TrustManager(x509Tm, deviceCredentialsService); + return x509TmWrapper; + } + + static class ThingsboardMqttX509TrustManager implements X509TrustManager { + + private final X509TrustManager trustManager; + private DeviceCredentialsService deviceCredentialsService; + + ThingsboardMqttX509TrustManager(X509TrustManager trustManager, DeviceCredentialsService deviceCredentialsService) { + this.trustManager = trustManager; + this.deviceCredentialsService = deviceCredentialsService; + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return trustManager.getAcceptedIssuers(); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, + String authType) throws CertificateException { + trustManager.checkServerTrusted(chain, authType); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, + String authType) throws CertificateException { + for (X509Certificate cert : chain) { + try { + String strCert = SslUtil.getX509CertificateString(cert); + String sha3Hash = EncryptionUtil.getSha3Hash(strCert); + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(sha3Hash); + if (deviceCredentials == null) { + throw new CertificateException("Invalid Device Certificate"); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } } diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index b4d8108a94..437ce036bf 100644 --- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -18,11 +18,13 @@ package org.thingsboard.server.transport.mqtt; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.mqtt.*; +import io.netty.handler.ssl.SslHandler; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; import lombok.extern.slf4j.Slf4j; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.security.DeviceTokenCredentials; +import org.thingsboard.server.common.data.security.DeviceX509Credentials; import org.thingsboard.server.common.msg.session.AdaptorToSessionActorMsg; import org.thingsboard.server.common.msg.session.BasicToDeviceActorSessionMsg; import org.thingsboard.server.common.msg.session.MsgType; @@ -30,9 +32,13 @@ import org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg; import org.thingsboard.server.common.transport.SessionMsgProcessor; import org.thingsboard.server.common.transport.adaptor.AdaptorException; import org.thingsboard.server.common.transport.auth.DeviceAuthService; +import org.thingsboard.server.dao.EncryptionUtil; import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; import org.thingsboard.server.transport.mqtt.session.MqttSessionCtx; +import org.thingsboard.server.transport.mqtt.util.SslUtil; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; @@ -57,12 +63,15 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private final String sessionId; private final MqttTransportAdaptor adaptor; private final SessionMsgProcessor processor; + private final SslHandler sslHandler; - public MqttTransportHandler(SessionMsgProcessor processor, DeviceAuthService authService, MqttTransportAdaptor adaptor) { + public MqttTransportHandler(SessionMsgProcessor processor, DeviceAuthService authService, + MqttTransportAdaptor adaptor, SslHandler sslHandler) { this.processor = processor; this.adaptor = adaptor; this.sessionCtx = new MqttSessionCtx(processor, authService, adaptor); this.sessionId = sessionCtx.getSessionId().toUidStr(); + this.sslHandler = sslHandler; } @Override @@ -197,6 +206,15 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private void processConnect(ChannelHandlerContext ctx, MqttConnectMessage msg) { log.info("[{}] Processing connect msg for client: {}!", sessionId, msg.payload().clientIdentifier()); + X509Certificate cert; + if (sslHandler != null && (cert = getX509Certificate()) != null) { + processX509CertConnect(ctx, cert); + } else { + processAuthTokenConnect(ctx, msg); + } + } + + private void processAuthTokenConnect(ChannelHandlerContext ctx, MqttConnectMessage msg) { String userName = msg.payload().userName(); if (StringUtils.isEmpty(userName)) { ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD)); @@ -209,6 +227,35 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } + private void processX509CertConnect(ChannelHandlerContext ctx, X509Certificate cert) { + try { + String strCert = SslUtil.getX509CertificateString(cert); + String sha3Hash = EncryptionUtil.getSha3Hash(strCert); + if (sessionCtx.login(new DeviceX509Credentials(sha3Hash))) { + ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_ACCEPTED)); + } else { + ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_NOT_AUTHORIZED)); + ctx.close(); + } + } catch (Exception e) { + ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_NOT_AUTHORIZED)); + ctx.close(); + } + } + + private X509Certificate getX509Certificate() { + try { + X509Certificate[] certChain = sslHandler.engine().getSession().getPeerCertificateChain(); + if (certChain.length > 0) { + return certChain[0]; + } + } catch (SSLPeerUnverifiedException e) { + log.warn(e.getMessage()); + return null; + } + return null; + } + private void processDisconnect(ChannelHandlerContext ctx) { ctx.close(); } diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java index 0c603095f6..7eeb955dfe 100644 --- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java +++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java @@ -55,13 +55,15 @@ public class MqttTransportServerInitializer extends ChannelInitializer + + org.thingsboard + dao + org.springframework.boot spring-boot-autoconfigure + + org.bouncycastle + bcprov-jdk15on + diff --git a/ui/src/app/device/device-credentials.controller.js b/ui/src/app/device/device-credentials.controller.js index 42b6696e61..acc1106c8c 100644 --- a/ui/src/app/device/device-credentials.controller.js +++ b/ui/src/app/device/device-credentials.controller.js @@ -24,7 +24,7 @@ export default function ManageDeviceCredentialsController(deviceService, $scope, value: 'ACCESS_TOKEN' }, { - name: 'X.509 Certificate (Coming soon)', + name: 'X.509 Certificate', value: 'X509_CERTIFICATE' } ]; @@ -35,6 +35,7 @@ export default function ManageDeviceCredentialsController(deviceService, $scope, vm.valid = valid; vm.cancel = cancel; vm.save = save; + vm.clear = clear; loadDeviceCredentials(); @@ -50,10 +51,16 @@ export default function ManageDeviceCredentialsController(deviceService, $scope, function valid() { return vm.deviceCredentials && - vm.deviceCredentials.credentialsType === 'ACCESS_TOKEN' && + (vm.deviceCredentials.credentialsType === 'ACCESS_TOKEN' + || vm.deviceCredentials.credentialsType === 'X509_CERTIFICATE') + && vm.deviceCredentials.credentialsId && vm.deviceCredentials.credentialsId.length > 0; } + function clear() { + vm.deviceCredentials.credentialsId = null; + } + function save() { deviceService.saveDeviceCredentials(vm.deviceCredentials).then(function success(deviceCredentials) { vm.deviceCredentials = deviceCredentials; diff --git a/ui/src/app/device/device-credentials.tpl.html b/ui/src/app/device/device-credentials.tpl.html index 5bfc2c08ff..ddd9defe51 100644 --- a/ui/src/app/device/device-credentials.tpl.html +++ b/ui/src/app/device/device-credentials.tpl.html @@ -33,7 +33,8 @@
- + {{credentialsType.name}} @@ -48,6 +49,14 @@
device.access-token-invalid
+ + +