Commit 7004176666a32b9367300fe5a86b856e5acc555f
Committed by
GitHub
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 |
common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceX509Credentials.java
0 → 100644
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 | } |
@@ -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." |
tools/src/main/shell/onewaysslmqttclient.py
0 → 100644
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." |
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", |