Merge pull request #9145 from YevhenBondarenko/feature/mqtts-cert

download mqtts cert improvements
This commit is contained in:
Andrew Shvayka 2023-09-05 18:33:09 +03:00 committed by GitHub
commit 4e1e9dcc7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 135 additions and 12 deletions

View File

@ -27,6 +27,7 @@ import org.mockito.AdditionalAnswers;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
@ -43,13 +44,15 @@ import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileCon
import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.dao.device.DeviceDao; import org.thingsboard.server.dao.device.DeviceDao;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.COAP; import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.COAP;
@ -59,21 +62,43 @@ import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTP;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTPS; import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTPS;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTT; import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTT;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTTS; import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTTS;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.PEM_CERT_FILE_NAME;
@TestPropertySource(properties = { @TestPropertySource(properties = {
"device.connectivity.https.enabled=true", "device.connectivity.https.enabled=true",
"device.connectivity.mqtts.enabled=true", "device.connectivity.mqtts.enabled=true",
"device.connectivity.mqtts.pem_cert_file=/tmp/" + PEM_CERT_FILE_NAME,
"device.connectivity.coaps.enabled=true", "device.connectivity.coaps.enabled=true",
}) })
@ContextConfiguration(classes = {DeviceConnectivityControllerTest.Config.class}) @ContextConfiguration(classes = {DeviceConnectivityControllerTest.Config.class})
@DaoSqlTest @DaoSqlTest
public class DeviceConnectivityControllerTest extends AbstractControllerTest { public class DeviceConnectivityControllerTest extends AbstractControllerTest {
static final TypeReference<PageData<Device>> PAGE_DATA_DEVICE_TYPE_REF = new TypeReference<>() {
};
private static final String DEVICE_TELEMETRY_TOPIC = "v1/devices/customTopic"; private static final String DEVICE_TELEMETRY_TOPIC = "v1/devices/customTopic";
private static final String CHECK_DOCUMENTATION = "Check documentation"; private static final String CHECK_DOCUMENTATION = "Check documentation";
private static final String CERT = "-----BEGIN CERTIFICATE-----\n" +
"MIIBfzCCASmgAwIBAgIUC1dtaskm/SJLmFE2Ae+YojArg+swDQYJKoZIhvcNAQEL\n" +
"BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMDgyODEzMTAzM1oXDTI0MDgy\n" +
"NzEzMTAzM1owFDESMBAGA1UEAwwJbG9jYWxob3N0MFwwDQYJKoZIhvcNAQEBBQAD\n" +
"SwAwSAJBANpcs46MavFdv7onsxH178YgK5XbpMqzx8AKaLMP2X6UEXN0nlt5mpX5\n" +
"uCJmSwVaFn6lwTm8ThXFYOBydOQImIsCAwEAAaNTMFEwHQYDVR0OBBYEFDvN49bI\n" +
"LaWMmUZ+cMboWAaozfXTMB8GA1UdIwQYMBaAFDvN49bILaWMmUZ+cMboWAaozfXT\n" +
"MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADQQAhIQL8zPvIhQvHJocU\n" +
"tnSmDAE0iR2rJVkousA+LiORE9BnuBtBUEv5SvFUv3VYUWA0eYFoyatpDHByIm6e\n" +
"/+1c\n" +
"-----END CERTIFICATE-----\n";
private static final String P_KEY = "-----BEGIN PRIVATE KEY-----\n" +
"MIIBVgIBADANBgkqhkiG9w0BAQEFAASCAUAwggE8AgEAAkEA2lyzjoxq8V2/uiez\n" +
"EfXvxiArldukyrPHwAposw/ZfpQRc3SeW3malfm4ImZLBVoWfqXBObxOFcVg4HJ0\n" +
"5AiYiwIDAQABAkEA1DYhPljSmc2dRcHNMphLtMWQ9iumpGRBrS2wgMzXdz2NF2+0\n" +
"4cicaaL06/Cw6XXx43s8cn7e1xZAkGtNRQuqMQIhAPbrqrcYsropURpI5HSemeha\n" +
"MJA3i67ZFaom39VSrNKJAiEA4mQ0qFKxFSh2xAOqDWDRkiCgdOS00J6hgrYJRPcI\n" +
"nXMCIQDBHGjkT72gGKYkT3PUvSGTdc3bTIXDFmZ6L3MJTGJ7OQIhAKO+6r9coCy3\n" +
"ib+ZDuSCRNK2upgR3B6Qvi020VmKfDa1AiBhCgpBlClv5OjnmC42EGxxFOaZtNQQ\n" +
"C3swkUdrR3pezg==\n" +
"-----END PRIVATE KEY-----\n";
ListeningExecutorService executor; ListeningExecutorService executor;
private Tenant savedTenant; private Tenant savedTenant;
@ -337,4 +362,38 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
assertThat(commands).hasSize(1); assertThat(commands).hasSize(1);
assertThat(commands.get(COAP).get(COAPS).asText()).isEqualTo(CHECK_DOCUMENTATION); assertThat(commands.get(COAP).get(COAPS).asText()).isEqualTo(CHECK_DOCUMENTATION);
} }
@Test
@DirtiesContext
public void testDownloadMqttCert() throws Exception {
Path path = Files.createFile(Path.of("/tmp/" + PEM_CERT_FILE_NAME));
Files.writeString(path, CERT);
try {
String downloadedCert = doGet("/api/device-connectivity/mqtts/certificate/download", String.class);
Assert.assertEquals(CERT, downloadedCert);
} finally {
Files.deleteIfExists(path);
}
}
@Test
@DirtiesContext
public void testDownloadMqttCertFromFileWithPrivateKey() throws Exception {
Path path = Files.createFile(Path.of("/tmp/" + PEM_CERT_FILE_NAME));
Files.writeString(path, CERT + P_KEY);
try {
String downloadedCert = doGet("/api/device-connectivity/mqtts/certificate/download", String.class);
Assert.assertEquals(CERT, downloadedCert);
} finally {
Files.deleteIfExists(path);
}
}
@Test
@DirtiesContext
public void testDownloadMqttCertWithoutCertFile() throws Exception {
doGet("/api/device-connectivity/mqtts/certificate/download").andExpect(status().isNotFound());
}
} }

View File

@ -79,6 +79,10 @@
<groupId>org.postgresql</groupId> <groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId> <artifactId>postgresql</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>

View File

@ -19,13 +19,14 @@ import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
@Configuration @Configuration
@ConfigurationProperties(prefix = "device") @ConfigurationProperties(prefix = "device")
@Data @Data
public class DeviceConnectivityConfiguration { public class DeviceConnectivityConfiguration {
private Map<String, DeviceConnectivityInfo> connectivity; private Map<String, DeviceConnectivityInfo> connectivity = new HashMap<>();
public boolean isEnabled(String protocol) { public boolean isEnabled(String protocol) {
var info = connectivity.get(protocol); var info = connectivity.get(protocol);

View File

@ -19,8 +19,10 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.openssl.PEMParser;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
@ -35,11 +37,18 @@ import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.dao.util.DeviceConnectivityUtil; import org.thingsboard.server.dao.util.DeviceConnectivityUtil;
import javax.annotation.PostConstruct;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Base64;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.CHECK_DOCUMENTATION; import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.CHECK_DOCUMENTATION;
@ -59,6 +68,8 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService
public static final String INCORRECT_DEVICE_ID = "Incorrect deviceId "; public static final String INCORRECT_DEVICE_ID = "Incorrect deviceId ";
public static final String DEFAULT_DEVICE_TELEMETRY_TOPIC = "v1/devices/me/telemetry"; public static final String DEFAULT_DEVICE_TELEMETRY_TOPIC = "v1/devices/me/telemetry";
private final Map<String, Resource> certs = new ConcurrentHashMap<>();
@Autowired @Autowired
private DeviceCredentialsService deviceCredentialsService; private DeviceCredentialsService deviceCredentialsService;
@ -68,6 +79,19 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService
@Autowired @Autowired
private DeviceConnectivityConfiguration deviceConnectivityConfiguration; private DeviceConnectivityConfiguration deviceConnectivityConfiguration;
@PostConstruct
private void init() {
DeviceConnectivityInfo mqtts = deviceConnectivityConfiguration.getConnectivity().get(MQTTS);
if (mqtts != null && mqtts.isEnabled()) {
String certFilePath = mqtts.getPemCertFile();
if (StringUtils.isBlank(certFilePath) || !ResourceUtils.resourceExists(this, certFilePath)) {
String error = StringUtils.isBlank(certFilePath) ? "path is empty" : "file is not exists";
log.error("MQTTS is enabled but cert {}!", error);
}
}
}
@Override @Override
public JsonNode findDevicePublishTelemetryCommands(String baseUrl, Device device) throws URISyntaxException { public JsonNode findDevicePublishTelemetryCommands(String baseUrl, Device device) throws URISyntaxException {
DeviceId deviceId = device.getId(); DeviceId deviceId = device.getId();
@ -115,15 +139,50 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService
@Override @Override
public Resource getPemCertFile(String protocol) { public Resource getPemCertFile(String protocol) {
String certFilePath = deviceConnectivityConfiguration.getConnectivity() return certs.computeIfAbsent(protocol, key -> {
.get(protocol) String certFilePath = deviceConnectivityConfiguration.getConnectivity()
.getPemCertFile(); .get(protocol)
.getPemCertFile();
if (StringUtils.isNotBlank(certFilePath) && ResourceUtils.resourceExists(this, certFilePath)) {
try {
return getCert(certFilePath);
} catch (Exception e) {
String msg = String.format("Failed to read %s server certificate!", protocol);
log.warn(msg);
throw new RuntimeException(msg, e);
}
} else {
return null;
}
});
}
if (StringUtils.isNotBlank(certFilePath) && ResourceUtils.resourceExists(this, certFilePath)) { private Resource getCert(String path) throws Exception {
return new ClassPathResource(certFilePath); StringBuilder pemContentBuilder = new StringBuilder();
} else {
return null; try (InputStream inStream = ResourceUtils.getInputStream(this, path);
PEMParser pemParser = new PEMParser(new InputStreamReader(inStream))) {
Object object;
while ((object = pemParser.readObject()) != null) {
if (object instanceof X509CertificateHolder) {
var certHolder = (X509CertificateHolder) object;
String certBase64 = Base64.getEncoder().encodeToString(certHolder.getEncoded());
pemContentBuilder.append("-----BEGIN CERTIFICATE-----\n");
int index = 0;
while (index < certBase64.length()) {
pemContentBuilder.append(certBase64, index, Math.min(index + 64, certBase64.length()));
pemContentBuilder.append("\n");
index += 64;
}
pemContentBuilder.append("-----END CERTIFICATE-----\n");
}
}
} }
return new ByteArrayResource(pemContentBuilder.toString().getBytes(StandardCharsets.UTF_8));
} }
private JsonNode getHttpTransportPublishCommands(String defaultHostname, DeviceCredentials deviceCredentials) throws URISyntaxException { private JsonNode getHttpTransportPublishCommands(String defaultHostname, DeviceCredentials deviceCredentials) throws URISyntaxException {