Commit 4aabf936f1ee79324c25693f5f311030e51e9841

Authored by Valerii Sosliuk
1 parent e25bd4f0

X509 certificate support implemented

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