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 70277a1c4d..18cdb588ce 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/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 976f68821c..288914822d 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -76,14 +76,10 @@ mqtt: adaptor: "${MQTT_ADAPTOR_NAME:JsonMqttAdaptor}" timeout: "${MQTT_TIMEOUT:10000}" # Uncomment the following lines to enable ssl for MQTT -# ssl: -# key-store: keystore/mqttserver.jks -# key-store-password: password -# keyStoreType: JKS -# TrustStore can be the same as KeyStore -# trust-store: keystore/mqttserver.jks -# trust-store-password: password -# trustStoreType: JKS + ssl: + key_store: keystore/mqttserver.jks + key_store_password: password + key_store_type: JKS # CoAP server parameters coap: 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 a340b1b66f..e37bc0d5a7 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 6448d34413..78805ff81a 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..a7f23a811d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceX509Credentials.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2017 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 465fa092a4..199da1155c 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 6a4e0cbbfa..fa98b86b59 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..0ce5ac2f49 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/EncryptionUtil.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2017 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 trimNewLines(String input) { + return input.replaceAll("\n","").replaceAll("\r",""); + } + + public static String getSha3Hash(String data) { + String trimmedData = trimNewLines(data); + 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 1471f7c88f..10e329ad4d 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,12 +23,12 @@ 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; -import java.util.Optional; - import static org.thingsboard.server.dao.DaoUtil.getData; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateString; @@ -70,11 +70,21 @@ public class DeviceCredentialsServiceImpl implements DeviceCredentialsService { } private DeviceCredentials saveOrUpdare(DeviceCredentials deviceCredentials) { + if (deviceCredentials.getCredentialsType() == DeviceCredentialsType.X509_CERTIFICATE) { + formatCertData(deviceCredentials); + } log.trace("Executing updateDeviceCredentials [{}]", deviceCredentials); credentialsValidator.validate(deviceCredentials); return getData(deviceCredentialsDao.save(deviceCredentials)); } + private void formatCertData(DeviceCredentials deviceCredentials) { + String cert = EncryptionUtil.trimNewLines(deviceCredentials.getCredentialsValue()); + String sha3Hash = EncryptionUtil.getSha3Hash(cert); + deviceCredentials.setCredentialsId(sha3Hash); + deviceCredentials.setCredentialsValue(cert); + } + @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/docker/.env b/docker/.env index ca7f2b0357..534d4e86c2 100644 --- a/docker/.env +++ b/docker/.env @@ -1 +1,2 @@ -CASSANDRA_DATA_DIR=/home/docker/cassandra_volume +CASSANDRA_DATA_DIR=/Users/admin/data_dir +#CASSANDRA_DATA_DIR=/home/docker/cassandra_volume diff --git a/pom.xml b/pom.xml index 442caef8ee..909e255502 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..08d6816c44 100644 --- a/tools/src/main/shell/keygen.properties +++ b/tools/src/main/shell/keygen.properties @@ -1,7 +1,25 @@ -HOSTNAME="$(hostname)" +# +# Copyright © 2016-2017 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. +# + +DOMAIN_SUFFIX="$(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 d7cb75a9e4..1572d8908c 100755 --- a/tools/src/main/shell/keygen.sh +++ b/tools/src/main/shell/keygen.sh @@ -15,14 +15,57 @@ # limitations under the License. # +usage() { + echo "This script generates thingsboard server's ssl certificate" + echo "and optionally copies it to the server's resource directory." + echo "usage: ./keygen.sh [-c flag] [-d directory]" + echo " -c | --copy flag Set if copy keystore to server directory needed. Default value is true" + echo " -d | --dir directory Server keystore directory, where the generated keystore file will be copied." + echo " Default value is SERVER_KEYSTORE_DIR property from properties file" + echo " -p | --props | --properties file Properties file. default value is ./keygen.properties" + echo " -h | --help | ? Show this message" +} -. keygen.properties +COPY=true; +COPY_DIR= +PROPERTIES_FILE=keygen.properties + +while true; do + case "$1" in + -c | --copy) COPY=$2 ; + shift + ;; + -d | --dir | --directory) COPY_DIR=$2 ; + shift + ;; + -p | --props | --properties) PROPERTIES_FILE=$2 ; + shift + ;; + -h | --help | ?) usage + exit 0 + ;; + -- ) shift; + break + ;; + * ) break + ;; + esac + shift +done + +if [[ "$COPY" != true ]] && [[ "$COPY" != false ]]; then + usage +fi + +echo "copy: $COPY; copy_dir: $COPY_DIR; PROPERTIES_FILE=$PROPERTIES_FILE"; + +. $PROPERTIES_FILE echo "Generating SSL Key Pair..." keytool -genkeypair -v \ -alias $SERVER_KEY_ALIAS \ - -dname "CN=$HOSTNAME, OU=Thingsboard, O=Thingsboard, L=Piscataway, ST=NJ, C=US" \ + -dname "CN=$DOMAIN_SUFFIX, OU=Thingsboard, O=Thingsboard, L=Piscataway, ST=NJ, C=US" \ -keystore $SERVER_FILE_PREFIX.jks \ -keypass $PASSWORD \ -storepass $PASSWORD \ @@ -30,28 +73,46 @@ keytool -genkeypair -v \ -keysize 2048 \ -validity 9999 +status=$? +if [[ $status != 0 ]]; then + exit $status; +fi + keytool -export \ -alias $SERVER_KEY_ALIAS \ -keystore $SERVER_FILE_PREFIX.jks \ -file $CLIENT_TRUSTSTORE -rfc \ -storepass $PASSWORD -read -p "Do you want to copy $SERVER_FILE_PREFIX.jks to server directory? " yn - case $yn in - [Yy]) echo "Please, specify destination dir: " - read -p "(Default: $SERVER_KEYSTORE_DIR): " dir - if [[ ! -z $dir ]]; then - DESTINATION=$dir; - else - DESTINATION=$SERVER_KEYSTORE_DIR - fi; - cp $SERVER_FILE_PREFIX.jks $DESTINATION - if [ $? -ne 0 ]; then - echo "Failed to copy keystore file." - else - echo "File copied successfully." - fi - break;; - * ) ;; - esac -echo "Done." +status=$? +if [[ $status != 0 ]]; then + exit $status; +fi + + +if [[ $COPY = true ]]; then + if [[ -z "$COPY_DIR" ]]; then + read -p "Do you want to copy $SERVER_FILE_PREFIX.jks to server directory? " yn + case $yn in + [Yy]) echo "Please, specify destination dir: " + read -p "(Default: $SERVER_KEYSTORE_DIR): " dir + if [[ ! -z $dir ]]; then + DESTINATION=$dir; + else + DESTINATION=$SERVER_KEYSTORE_DIR + fi; + break;; + * ) ;; + esac + else + DESTINATION=$COPY_DIR + fi + mkdir -p $DESTINATION + cp $SERVER_FILE_PREFIX.jks $DESTINATION + if [ $? -ne 0 ]; then + echo "Failed to copy keystore file." + else + echo "File copied successfully." + fi +fi +echo "Done." \ No newline at end of file diff --git a/tools/src/main/shell/onewaysslmqttclient.py b/tools/src/main/shell/onewaysslmqttclient.py new file mode 100644 index 0000000000..b0824e64d4 --- /dev/null +++ b/tools/src/main/shell/onewaysslmqttclient.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# +# Copyright © 2016-2017 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..4d986cdac9 --- /dev/null +++ b/tools/src/main/shell/securemqttclient.keygen.sh @@ -0,0 +1,89 @@ +#!/bin/sh +# +# Copyright © 2016-2017 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. +# + +usage() { + echo "This script generates client public/private rey pair, extracts them to a no-password RSA pem file," + echo "and also imports server public key to client trust store" + echo "usage: ./securemqttclient.keygen.sh [-p file]" + echo " -p | --props | --properties file Properties file. default value is ./keygen.properties" + echo " -h | --help | ? Show this message" +} + +PROPERTIES_FILE=keygen.properties + +while true; do + case "$1" in + -p | --props | --properties) PROPERTIES_FILE=$2 ; + shift + ;; + -h | --help | ?) usage + exit 0 + ;; + -- ) shift; + break + ;; + * ) break + ;; + esac + shift +done + +. $PROPERTIES_FILE + +echo "Generating SSL Key Pair..." + +keytool -genkeypair -v \ + -alias $CLIENT_KEY_ALIAS \ + -dname "CN=$DOMAIN_SUFFIX, 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/simplemqttclient.py b/tools/src/main/shell/simplemqttclient.py index 91b3e3410c..9ec3250ca2 100644 --- a/tools/src/main/shell/simplemqttclient.py +++ b/tools/src/main/shell/simplemqttclient.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # # Copyright © 2016-2017 The Thingsboard Authors # 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 5a103ef181..d51df36f09 100644 --- a/tools/src/main/shell/securemqttclient.py +++ b/tools/src/main/shell/twowaysslmqttclient.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # # Copyright © 2016-2017 The Thingsboard Authors # @@ -41,9 +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.username_pw_set("TEST_TOKEN") +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_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 1fc9e1d6e2..da478ebd40 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. @@ -37,31 +45,27 @@ import java.security.KeyStore; public class MqttSslHandlerProvider { public static final String TLS = "TLS"; - @Value("${mqtt.ssl.key-store}") + @Value("${mqtt.ssl.key_store}") private String keyStoreFile; - @Value("${mqtt.ssl.key-store-password}") + @Value("${mqtt.ssl.key_store_password}") private String keyStorePassword; - @Value("${mqtt.ssl.keyStoreType}") + @Value("${mqtt.ssl.key_store_type}") private String keyStoreType; - @Value("${mqtt.ssl.trust-store}") - private String trustStoreFile; - @Value("${mqtt.ssl.trust-store-password}") - private String trustStorePassword; - @Value("${mqtt.ssl.trustStoreType}") - private String trustStoreType; + @Autowired + private DeviceCredentialsService deviceCredentialsService; public SslHandler getSslHandler() { try { URL ksUrl = Resources.getResource(keyStoreFile); File ksFile = new File(ksUrl.toURI()); - URL tsUrl = Resources.getResource(trustStoreFile); + URL tsUrl = Resources.getResource(keyStoreFile); File tsFile = new File(tsUrl.toURI()); TrustManagerFactory tmFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - KeyStore trustStore = KeyStore.getInstance(trustStoreType); - trustStore.load(new FileInputStream(tsFile), trustStorePassword.toCharArray()); + KeyStore trustStore = KeyStore.getInstance(keyStoreType); + trustStore.load(new FileInputStream(tsFile), keyStorePassword.toCharArray()); tmFactory.init(trustStore); KeyStore ks = KeyStore.getInstance(keyStoreType); @@ -71,13 +75,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 +93,57 @@ 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; + } + } + return new ThingsboardMqttX509TrustManager(x509Tm, deviceCredentialsService); + } + + 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 { + DeviceCredentials deviceCredentials = null; + for (X509Certificate cert : chain) { + try { + String strCert = SslUtil.getX509CertificateString(cert); + String sha3Hash = EncryptionUtil.getSha3Hash(strCert); + deviceCredentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(sha3Hash); + if (deviceCredentials != null && strCert.equals(deviceCredentials.getCredentialsValue())) { + break; + } + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } + if (deviceCredentials == null) { + throw new CertificateException("Invalid Device Certificate"); + } + } + } } 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 a10b216642..30b37e3ff6 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 0aa2874a89..323ed1e4b8 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 c89ab49952..537df5d1bb 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,8 +51,17 @@ export default function ManageDeviceCredentialsController(deviceService, $scope, function valid() { return vm.deviceCredentials && - vm.deviceCredentials.credentialsType === 'ACCESS_TOKEN' && - vm.deviceCredentials.credentialsId && vm.deviceCredentials.credentialsId.length > 0; + (vm.deviceCredentials.credentialsType === 'ACCESS_TOKEN' + && vm.deviceCredentials.credentialsId + && vm.deviceCredentials.credentialsId.length > 0 + || vm.deviceCredentials.credentialsType === 'X509_CERTIFICATE' + && vm.deviceCredentials.credentialsValue + && vm.deviceCredentials.credentialsValue.length > 0); + } + + function clear() { + vm.deviceCredentials.credentialsId = null; + vm.deviceCredentials.credentialsValue = null; } function save() { diff --git a/ui/src/app/device/device-credentials.tpl.html b/ui/src/app/device/device-credentials.tpl.html index cc1ead0fd9..9dd4553b08 100644 --- a/ui/src/app/device/device-credentials.tpl.html +++ b/ui/src/app/device/device-credentials.tpl.html @@ -33,7 +33,8 @@ device.credentials-type - + {{credentialsType.name}} @@ -48,6 +49,14 @@ device.access-token-invalid + + device.rsa-key + + + device.rsa-key-required + + diff --git a/ui/src/locale/en_US.json b/ui/src/locale/en_US.json index ccbc3e7629..f042993493 100644 --- a/ui/src/locale/en_US.json +++ b/ui/src/locale/en_US.json @@ -308,6 +308,8 @@ "access-token": "Access token", "access-token-required": "Access token is required.", "access-token-invalid": "Access token length must be from 1 to 20 characters.", + "rsa-key": "RSA public key", + "access-token-required": "RSA public key is required.", "secret": "Secret", "secret-required": "Secret is required.", "name": "Name",