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