Commit 7004176666a32b9367300fe5a86b856e5acc555f

Authored by Andrew Shvayka
Committed by GitHub
2 parents 39ca468f f649ef57

Merge pull request #32 from thingsboard/X_509

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