thingsboard-aplcache
Changes
application/src/main/java/org/thingsboard/server/service/security/device/DefaultDeviceAuthService.java 2(+2 -0)
common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsType.java 3(+2 -1)
common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceTokenCredentials.java 1(+0 -1)
common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceX509Credentials.java 39(+39 -0)
dao/pom.xml 4(+4 -0)
pom.xml 11(+11 -0)
tools/src/main/shell/keygen.sh 1(+1 -0)
tools/src/main/shell/onewaysslmqttclient.py 59(+59 -0)
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java 66(+64 -2)
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java 49(+48 -1)
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java 6(+4 -2)
transport/pom.xml 8(+8 -0)
ui/src/locale/en_US.json 2(+2 -0)
Details
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 cef73fe..dbc1920 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 3daa1e4..d388348 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 8ce9f00..3e9001b 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 0000000..f2be4c1
--- /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 + "]";
+ }
+}
dao/pom.xml 4(+4 -0)
diff --git a/dao/pom.xml b/dao/pom.xml
index a2edc7a..3b25c42 100644
--- a/dao/pom.xml
+++ b/dao/pom.xml
@@ -146,6 +146,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcprov-jdk15on</artifactId>
+ </dependency>
</dependencies>
<build>
<plugins>
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 da1a42f..f258122 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/device/DeviceCredentialsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java
index 19ad2d1..cc0e642 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/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 0000000..df456c8
--- /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;
+ }
+}
pom.xml 11(+11 -0)
diff --git a/pom.xml b/pom.xml
index b68bee7..226f915 100755
--- a/pom.xml
+++ b/pom.xml
@@ -69,6 +69,7 @@
<surfire.version>2.19.1</surfire.version>
<jar-plugin.version>3.0.2</jar-plugin.version>
<springfox-swagger.version>2.6.1</springfox-swagger.version>
+ <bouncycastle.version>1.56</bouncycastle.version>
</properties>
<modules>
@@ -689,6 +690,16 @@
<artifactId>springfox-swagger2</artifactId>
<version>${springfox-swagger.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcprov-jdk15on</artifactId>
+ <version>${bouncycastle.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcpkix-jdk15on</artifactId>
+ <version>${bouncycastle.version}</version>
+ </dependency>
</dependencies>
</dependencyManagement>
diff --git a/tools/src/main/shell/keygen.properties b/tools/src/main/shell/keygen.properties
index 6016bda..d27e0f5 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"
tools/src/main/shell/keygen.sh 1(+1 -0)
diff --git a/tools/src/main/shell/keygen.sh b/tools/src/main/shell/keygen.sh
index 25b3157..46e670d 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."
tools/src/main/shell/onewaysslmqttclient.py 59(+59 -0)
diff --git a/tools/src/main/shell/onewaysslmqttclient.py b/tools/src/main/shell/onewaysslmqttclient.py
new file mode 100644
index 0000000..a6efbd3
--- /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 0000000..1d6752c
--- /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/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 f7b38d0..6872c03 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 b4d8108..437ce03 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 0c60309..7eeb955 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<SocketCha
@Override
public void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
+ SslHandler sslHandler = null;
if (sslHandlerProvider != null) {
- pipeline.addLast(sslHandlerProvider.getSslHandler());
+ sslHandler = sslHandlerProvider.getSslHandler();
+ pipeline.addLast(sslHandler);
}
pipeline.addLast("decoder", new MqttDecoder());
pipeline.addLast("encoder", MqttEncoder.INSTANCE);
- MqttTransportHandler handler = new MqttTransportHandler(processor, authService, adaptor);
+ MqttTransportHandler handler = new MqttTransportHandler(processor, authService, adaptor, sslHandler);
pipeline.addLast(handler);
ch.closeFuture().addListener(handler);
}
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/SslUtil.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/SslUtil.java
new file mode 100644
index 0000000..8df1681
--- /dev/null
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/SslUtil.java
@@ -0,0 +1,49 @@
+/**
+ * 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.transport.mqtt.util;
+
+import lombok.extern.slf4j.Slf4j;
+import sun.misc.BASE64Encoder;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+
+/**
+ * @author Valerii Sosliuk
+ */
+@Slf4j
+public class SslUtil {
+
+ private SslUtil() {
+ }
+
+ public static String getX509CertificateString(X509Certificate cert) throws CertificateEncodingException, IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ BASE64Encoder encoder = new BASE64Encoder();
+ encoder.encodeBuffer(cert.getEncoded(), out);
+ return new String(out.toByteArray(), "UTF-8").trim();
+ }
+
+ public static String getX509CertificateString(javax.security.cert.X509Certificate cert)
+ throws javax.security.cert.CertificateEncodingException, IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ BASE64Encoder encoder = new BASE64Encoder();
+ encoder.encodeBuffer(cert.getEncoded(), out);
+ return new String(out.toByteArray(), "UTF-8").trim();
+ }
+}
transport/pom.xml 8(+8 -0)
diff --git a/transport/pom.xml b/transport/pom.xml
index 4b7c8ab..dd71478 100644
--- a/transport/pom.xml
+++ b/transport/pom.xml
@@ -42,9 +42,17 @@
<dependencies>
<dependency>
+ <groupId>org.thingsboard</groupId>
+ <artifactId>dao</artifactId>
+ </dependency>
+ <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcprov-jdk15on</artifactId>
+ </dependency>
</dependencies>
</project>
diff --git a/ui/src/app/device/device-credentials.controller.js b/ui/src/app/device/device-credentials.controller.js
index 42b6696..acc1106 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 5bfc2c0..ddd9def 100644
--- a/ui/src/app/device/device-credentials.tpl.html
+++ b/ui/src/app/device/device-credentials.tpl.html
@@ -33,7 +33,8 @@
<fieldset ng-disabled="loading || vm.isReadOnly">
<md-input-container class="md-block">
<label translate>device.credentials-type</label>
- <md-select ng-disabled="loading || vm.isReadOnly" ng-model="vm.deviceCredentials.credentialsType">
+ <md-select ng-disabled="loading || vm.isReadOnly" ng-model="vm.deviceCredentials.credentialsType"
+ ng-change="vm.clear()">
<md-option ng-repeat="credentialsType in vm.credentialsTypes" value="{{credentialsType.value}}">
{{credentialsType.name}}
</md-option>
@@ -48,6 +49,14 @@
<div translate ng-message="pattern">device.access-token-invalid</div>
</div>
</md-input-container>
+ <md-input-container class="md-block" ng-if="vm.deviceCredentials.credentialsType === 'X509_CERTIFICATE'">
+ <label translate>device.rsa-key</label>
+ <textarea required name="rsaKey" ng-model="vm.deviceCredentials.credentialsId"
+ cols="15" rows="5" />
+ <div ng-messages="theForm.rsaKey.$error">
+ <div translate ng-message="required">device.rsa-key-required</div>
+ </div>
+ </md-input-container>
</fieldset>
</div>
</md-dialog-content>
ui/src/locale/en_US.json 2(+2 -0)
diff --git a/ui/src/locale/en_US.json b/ui/src/locale/en_US.json
index 5de8938..832a6ed 100644
--- a/ui/src/locale/en_US.json
+++ b/ui/src/locale/en_US.json
@@ -294,6 +294,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",