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.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
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.MqttDeviceProfileTransportConfiguration;
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.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.dao.device.DeviceDao;
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.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
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.MQTT;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTTS;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.PEM_CERT_FILE_NAME;
@TestPropertySource(properties = {
"device.connectivity.https.enabled=true",
"device.connectivity.mqtts.enabled=true",
"device.connectivity.mqtts.pem_cert_file=/tmp/" + PEM_CERT_FILE_NAME,
"device.connectivity.coaps.enabled=true",
})
@ContextConfiguration(classes = {DeviceConnectivityControllerTest.Config.class})
@DaoSqlTest
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 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;
private Tenant savedTenant;
@ -337,4 +362,38 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
assertThat(commands).hasSize(1);
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>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<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.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
@Configuration
@ConfigurationProperties(prefix = "device")
@Data
public class DeviceConnectivityConfiguration {
private Map<String, DeviceConnectivityInfo> connectivity;
private Map<String, DeviceConnectivityInfo> connectivity = new HashMap<>();
public boolean isEnabled(String 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.ObjectNode;
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.core.io.ClassPathResource;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
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.dao.util.DeviceConnectivityUtil;
import javax.annotation.PostConstruct;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import static org.thingsboard.server.dao.service.Validator.validateId;
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 DEFAULT_DEVICE_TELEMETRY_TOPIC = "v1/devices/me/telemetry";
private final Map<String, Resource> certs = new ConcurrentHashMap<>();
@Autowired
private DeviceCredentialsService deviceCredentialsService;
@ -68,6 +79,19 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService
@Autowired
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
public JsonNode findDevicePublishTelemetryCommands(String baseUrl, Device device) throws URISyntaxException {
DeviceId deviceId = device.getId();
@ -115,15 +139,50 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService
@Override
public Resource getPemCertFile(String protocol) {
String certFilePath = deviceConnectivityConfiguration.getConnectivity()
.get(protocol)
.getPemCertFile();
return certs.computeIfAbsent(protocol, key -> {
String certFilePath = deviceConnectivityConfiguration.getConnectivity()
.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)) {
return new ClassPathResource(certFilePath);
} else {
return null;
private Resource getCert(String path) throws Exception {
StringBuilder pemContentBuilder = new StringBuilder();
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 {