Browse Source

Merge remote-tracking branch 'alandiak/feature/x509-device-cert-impr' into feature/x509-device-cert-impr

pull/7935/head
deaflynx 4 years ago
parent
commit
038a714e87
  1. 10
      application/src/main/data/upgrade/3.4.4/schema_update.sql
  2. 124
      application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java
  3. 19
      application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java
  4. 180
      application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java
  5. 5
      common/cluster-api/src/main/proto/queue.proto
  6. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java
  7. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java
  8. 5
      common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java
  9. 3
      common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileProvisionType.java
  10. 3
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java
  11. 37
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/X509CertificateChainProvisionConfiguration.java
  12. 7
      common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java
  13. 68
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java
  14. 6
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java
  15. 9
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java
  16. 28
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java
  17. 3
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java
  18. 22
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCacheKey.java
  19. 2
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java
  20. 1
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileEvictEvent.java
  21. 76
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java
  22. 1
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  23. 5
      dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java
  24. 57
      dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java
  25. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java
  26. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java
  27. 4
      dao/src/main/resources/sql/schema-entities.sql

10
application/src/main/data/upgrade/3.4.4/schema_update.sql

@ -347,3 +347,13 @@ END
$$;
-- ALARM FUNCTIONS END
-- DEVICE PROFILE CERTIFICATE START
ALTER TABLE device_profile
ADD COLUMN IF NOT EXISTS certificate_hash varchar,
DROP CONSTRAINT IF EXISTS device_profile_credentials_hash_unq_key,
ADD CONSTRAINT device_profile_credentials_hash_unq_key UNIQUE (certificate_hash);
-- DEVICE PROFILE CERTIFICATE END

124
application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java

@ -25,6 +25,7 @@ import com.google.common.util.concurrent.MoreExecutors;
import com.google.protobuf.ByteString;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cache.ota.OtaPackageDataCache;
@ -33,6 +34,7 @@ import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.OtaPackage;
@ -48,6 +50,7 @@ import org.thingsboard.server.common.data.device.data.Lwm2mDeviceTransportConfig
import org.thingsboard.server.common.data.device.data.PowerMode;
import org.thingsboard.server.common.data.device.data.PowerSavingConfiguration;
import org.thingsboard.server.common.data.device.profile.ProvisionDeviceProfileCredentials;
import org.thingsboard.server.common.data.device.profile.X509CertificateChainProvisionConfiguration;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
@ -65,7 +68,9 @@ import org.thingsboard.server.common.msg.EncryptionUtil;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.transport.util.SslUtil;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceProvisionService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.device.provision.ProvisionFailedException;
@ -101,6 +106,11 @@ import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.resource.TbResourceService;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@ -108,6 +118,8 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.thingsboard.server.service.transport.BasicCredentialsValidationResult.PASSWORD_MISMATCH;
@ -128,6 +140,7 @@ public class DefaultTransportApiService implements TransportApiService {
private final TbTenantProfileCache tenantProfileCache;
private final TbApiUsageStateService apiUsageStateService;
private final DeviceService deviceService;
private final DeviceProfileService deviceProfileService;
private final RelationService relationService;
private final DeviceCredentialsService deviceCredentialsService;
private final DbCallbackExecutorService dbCallbackExecutorService;
@ -159,6 +172,9 @@ public class DefaultTransportApiService implements TransportApiService {
} else if (transportApiRequestMsg.hasValidateX509CertRequestMsg()) {
ValidateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getValidateX509CertRequestMsg();
result = validateCredentials(msg.getHash(), DeviceCredentialsType.X509_CERTIFICATE);
} else if (transportApiRequestMsg.hasValidateOrCreateX509CertRequestMsg()) {
TransportProtos.ValidateOrCreateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getValidateOrCreateX509CertRequestMsg();
result = validateOrCreateDeviceX509Certificate(msg.getCertificateChain(), DeviceCredentialsType.X509_CERTIFICATE);
} else if (transportApiRequestMsg.hasGetOrCreateDeviceRequestMsg()) {
result = handle(transportApiRequestMsg.getGetOrCreateDeviceRequestMsg());
} else if (transportApiRequestMsg.hasEntityProfileRequestMsg()) {
@ -226,6 +242,52 @@ public class DefaultTransportApiService implements TransportApiService {
}
}
protected ListenableFuture<TransportApiResponseMsg> validateOrCreateDeviceX509Certificate(String certChain, DeviceCredentialsType credentialsType) {
List<String> chain = convertX509CertificateChainToList(certChain);
String updateDeviceCertificateValue = chain.get(0);
String updateDeviceCertificateHash = EncryptionUtil.getSha3Hash(updateDeviceCertificateValue);
String deviceCommonName = "";
try {
deviceCommonName = SslUtil.parseCommonName(readCertFile(chain.get(0)));
} catch (Exception ignored) {
}
for (String certificateValue : chain) {
String certificateHash = EncryptionUtil.getSha3Hash(certificateValue);
DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(certificateHash);
if (credentials != null && credentials.getCredentialsType() == credentialsType) {
return getDeviceInfo(credentials);
}
DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileByCertificateHash(certificateHash);
if (deviceProfile != null) {
X509CertificateChainProvisionConfiguration x509Configuration = new X509CertificateChainProvisionConfiguration();
if (deviceProfile.getProfileData().getProvisionConfiguration() instanceof X509CertificateChainProvisionConfiguration) {
x509Configuration = (X509CertificateChainProvisionConfiguration) deviceProfile.getProfileData().getProvisionConfiguration();
}
String deviceName = extractDeviceNameFromCNByRegEx(deviceCommonName, x509Configuration.getCertificateRegExPattern());
if (deviceName == null) {
log.error("Device name cannot be unmatched from CN!");
return getEmptyTransportApiResponseFuture();
}
Device device = deviceService.findDeviceByTenantIdAndName(deviceProfile.getTenantId(), deviceName);
if (device != null) {
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), device.getId());
if (deviceCredentials != null && deviceCredentials.getCredentialsType() == credentialsType) {
deviceCredentials = updateDeviceCredentials(device.getTenantId(), deviceCredentials, updateDeviceCertificateValue, updateDeviceCertificateHash, credentialsType);
} else if (deviceCredentials == null) {
deviceCredentials = createDeviceCredentials(device.getTenantId(), device.getId(), updateDeviceCertificateValue, updateDeviceCertificateHash, credentialsType);
}
return getDeviceInfo(deviceCredentials);
} else if (deviceProfile.getProvisionType() == DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN && x509Configuration.isAllowCreateNewDevicesByX509Certificate()) {
Device savedDevice = createDevice(deviceProfile.getTenantId(), deviceProfile.getId(), deviceName, deviceProfile.getName());
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedDevice.getTenantId(), savedDevice.getId());
deviceCredentials = updateDeviceCredentials(savedDevice.getTenantId(), deviceCredentials, updateDeviceCertificateValue, updateDeviceCertificateHash, credentialsType);
return getDeviceInfo(deviceCredentials);
}
}
}
return getEmptyTransportApiResponseFuture();
}
private ListenableFuture<TransportApiResponseMsg> validateUserNameCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg mqtt) {
DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(mqtt.getUserName());
if (credentials != null) {
@ -665,4 +727,66 @@ public class DefaultTransportApiService implements TransportApiService {
private Long checkLong(Long l) {
return l != null ? l : 0;
}
private String extractDeviceNameFromCNByRegEx(String commonName, String regex) {
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(commonName);
if (matcher.find()) {
return matcher.group(0);
}
return null;
}
private List<String> convertX509CertificateChainToList(String certificateChain) {
List<String> chain = new ArrayList<>();
String regex = "-----BEGIN CERTIFICATE-----\\s*.*?\\s*-----END CERTIFICATE-----";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(certificateChain);
while (matcher.find()) {
chain.add(EncryptionUtil.certTrimNewLines(matcher.group()));
}
return chain;
}
private X509Certificate readCertFile(String fileContent) throws Exception {
X509Certificate certificate = null;
if (fileContent != null && !fileContent.trim().isEmpty()) {
fileContent = fileContent.replace("-----BEGIN CERTIFICATE-----", "")
.replace("-----END CERTIFICATE-----", "")
.replaceAll("\\s", "");
byte[] decoded = Base64.decodeBase64(fileContent);
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
try (InputStream inStream = new ByteArrayInputStream(decoded)) {
certificate = (X509Certificate) certFactory.generateCertificate(inStream);
}
}
return certificate;
}
private DeviceCredentials updateDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials, String certificateValue,
String certificateHash, DeviceCredentialsType credentialsType) {
deviceCredentials.setCredentialsId(certificateHash);
deviceCredentials.setCredentialsValue(certificateValue);
deviceCredentials.setCredentialsType(credentialsType);
return deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials);
}
private DeviceCredentials createDeviceCredentials(TenantId tenantId, DeviceId deviceId, String certificateValue,
String certificateHash, DeviceCredentialsType credentialsType) {
DeviceCredentials createDevCredentials = new DeviceCredentials();
createDevCredentials.setDeviceId(deviceId);
createDevCredentials.setCredentialsType(credentialsType);
createDevCredentials.setCredentialsId(certificateHash);
createDevCredentials.setCredentialsValue(certificateValue);
return deviceCredentialsService.createDeviceCredentials(tenantId, createDevCredentials);
}
private Device createDevice(TenantId tenantId, DeviceProfileId deviceProfileId, String deviceName, String type) {
Device device = new Device();
device.setTenantId(tenantId);
device.setDeviceProfileId(deviceProfileId);
device.setName(deviceName);
device.setType(type);
return deviceService.saveDevice(device);
}
}

19
application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java

@ -299,6 +299,25 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController
tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, new DataValidationException(msgError));
}
@Test
public void testSaveDeviceProfileWithSameCertificateHash() throws Exception {
DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile");
deviceProfile.setCertificateHash("Certificate Hash");
doPost("/api/deviceProfile", deviceProfile).andExpect(status().isOk());
DeviceProfile deviceProfile2 = this.createDeviceProfile("Device Profile 2");
deviceProfile2.setCertificateHash("Certificate Hash");
Mockito.reset(tbClusterService, auditLogService);
String msgError = "Device profile with such certificate hash already exists";
doPost("/api/deviceProfile", deviceProfile2)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString(msgError)));
testNotifyEntityEqualsOneTimeServiceNeverError(deviceProfile, savedTenant.getId(),
tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, new DataValidationException(msgError));
}
@Test
public void testChangeDeviceProfileTypeNull() throws Exception {
DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile");

180
application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java

@ -0,0 +1,180 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.transport;
import com.google.common.util.concurrent.Futures;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.thingsboard.server.cache.ota.OtaPackageDataCache;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.device.profile.X509CertificateChainProvisionConfiguration;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.common.msg.EncryptionUtil;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceProvisionService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.resource.TbResourceService;
import java.util.UUID;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@Slf4j
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = DefaultTransportApiService.class)
public class DefaultTransportApiServiceTest {
@MockBean
protected TbDeviceProfileCache deviceProfileCache;
@MockBean
protected TbTenantProfileCache tenantProfileCache;
@MockBean
protected TbApiUsageStateService apiUsageStateService;
@MockBean
protected DeviceService deviceService;
@MockBean
protected DeviceProfileService deviceProfileService;
@MockBean
protected RelationService relationService;
@MockBean
protected DeviceCredentialsService deviceCredentialsService;
@MockBean
protected DbCallbackExecutorService dbCallbackExecutorService;
@MockBean
protected TbClusterService tbClusterService;
@MockBean
protected DataDecodingEncodingService dataDecodingEncodingService;
@MockBean
protected DeviceProvisionService deviceProvisionService;
@MockBean
protected TbResourceService resourceService;
@MockBean
protected OtaPackageService otaPackageService;
@MockBean
protected OtaPackageDataCache otaPackageDataCache;
@MockBean
protected QueueService queueService;
@SpyBean
DefaultTransportApiService service;
private final String deviceCertificate = "-----BEGIN CERTIFICATE-----Device certificate value-----END CERTIFICATE-----";
private final String deviceProfileCertificate = "-----BEGIN CERTIFICATE-----Device profile certificate value-----END CERTIFICATE-----";
@Test
public void validateExistingDeviceX509Certificate() {
var device = createDevice();
when(deviceService.findDeviceByIdAsync(any(), any())).thenReturn(Futures.immediateFuture(device));
var deviceCredentials = createDeviceCredentials(deviceCertificate, device.getId());
when(deviceCredentialsService.findDeviceCredentialsByCredentialsId(any())).thenReturn(deviceCredentials);
service.validateOrCreateDeviceX509Certificate(deviceCertificate, DeviceCredentialsType.X509_CERTIFICATE);
verify(deviceCredentialsService, times(1)).findDeviceCredentialsByCredentialsId(any());
}
@Test
public void updateExistingDeviceX509Certificate() {
var deviceProfile = createDeviceProfile(deviceProfileCertificate);
when(deviceProfileService.findDeviceProfileByCertificateHash(any())).thenReturn(deviceProfile);
var device = createDevice();
when(deviceService.findDeviceByTenantIdAndName(any(), any())).thenReturn(device);
when(deviceService.findDeviceByIdAsync(any(), any())).thenReturn(Futures.immediateFuture(device));
var deviceCredentials = createDeviceCredentials(deviceCertificate, device.getId());
when(deviceCredentialsService.findDeviceCredentialsByDeviceId(any(), any())).thenReturn(deviceCredentials);
when(deviceCredentialsService.updateDeviceCredentials(any(), any())).thenReturn(deviceCredentials);
service.validateOrCreateDeviceX509Certificate(deviceProfileCertificate, DeviceCredentialsType.X509_CERTIFICATE);
verify(deviceProfileService, times(1)).findDeviceProfileByCertificateHash(any());
verify(deviceService, times(1)).findDeviceByTenantIdAndName(any(), any());
verify(deviceCredentialsService, times(1)).findDeviceCredentialsByDeviceId(any(), any());
verify(deviceCredentialsService, times(1)).updateDeviceCredentials(any(), any());
}
@Test
public void createDeviceByX509Provision() {
var deviceProfile = createDeviceProfile(deviceProfileCertificate);
when(deviceProfileService.findDeviceProfileByCertificateHash(any())).thenReturn(deviceProfile);
var device = createDevice();
when(deviceService.saveDevice(any())).thenReturn(device);
when(deviceService.findDeviceByIdAsync(any(), any())).thenReturn(Futures.immediateFuture(device));
var deviceCredentials = createDeviceCredentials(deviceCertificate, device.getId());
when(deviceCredentialsService.findDeviceCredentialsByDeviceId(any(), any())).thenReturn(deviceCredentials);
when(deviceCredentialsService.updateDeviceCredentials(any(), any())).thenReturn(deviceCredentials);
service.validateOrCreateDeviceX509Certificate(deviceProfileCertificate, DeviceCredentialsType.X509_CERTIFICATE);
verify(deviceProfileService, times(1)).findDeviceProfileByCertificateHash(any());
verify(deviceService, times(1)).findDeviceByTenantIdAndName(any(), any());
verify(deviceCredentialsService, times(1)).findDeviceCredentialsByDeviceId(any(), any());
verify(deviceCredentialsService, times(1)).updateDeviceCredentials(any(), any());
}
private DeviceCredentials createDeviceCredentials(String certificateValue, DeviceId deviceId) {
DeviceCredentials deviceCredentials = new DeviceCredentials();
deviceCredentials.setDeviceId(deviceId);
deviceCredentials.setCredentialsValue(certificateValue);
deviceCredentials.setCredentialsId(EncryptionUtil.getSha3Hash(certificateValue));
deviceCredentials.setCredentialsType(DeviceCredentialsType.X509_CERTIFICATE);
return deviceCredentials;
}
private DeviceProfile createDeviceProfile(String certificateValue) {
DeviceProfile deviceProfile = new DeviceProfile();
DeviceProfileData deviceProfileData = new DeviceProfileData();
X509CertificateChainProvisionConfiguration provision = new X509CertificateChainProvisionConfiguration();
provision.setCertificateValue(certificateValue);
provision.setCertificateRegExPattern("^$");
provision.setAllowCreateNewDevicesByX509Certificate(true);
deviceProfileData.setProvisionConfiguration(provision);
deviceProfile.setProfileData(deviceProfileData);
deviceProfile.setCertificateHash(EncryptionUtil.getSha3Hash(certificateValue));
deviceProfile.setProvisionType(DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN);
return deviceProfile;
}
private Device createDevice() {
Device device = new Device();
device.setId(new DeviceId(UUID.randomUUID()));
return device;
}
}

5
common/cluster-api/src/main/proto/queue.proto

@ -169,6 +169,10 @@ message ValidateDeviceX509CertRequestMsg {
string hash = 1;
}
message ValidateOrCreateDeviceX509CertRequestMsg {
string certificateChain = 1;
}
message ValidateBasicMqttCredRequestMsg {
string clientId = 1;
string userName = 2;
@ -897,6 +901,7 @@ message TransportApiRequestMsg {
GetDeviceRequestMsg deviceRequestMsg = 12;
GetDeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 13;
GetAllQueueRoutingInfoRequestMsg getAllQueueRoutingInfoRequestMsg = 14;
ValidateOrCreateDeviceX509CertRequestMsg validateOrCreateX509CertRequestMsg = 15;
}
/* Response from ThingsBoard Core Service to Transport Service */

2
common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java

@ -15,10 +15,10 @@
*/
package org.thingsboard.server.dao.device;
import com.fasterxml.jackson.databind.JsonNode;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import com.fasterxml.jackson.databind.JsonNode;
public interface DeviceCredentialsService {

2
common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java

@ -39,6 +39,8 @@ public interface DeviceProfileService extends EntityDaoService {
PageData<DeviceProfileInfo> findDeviceProfileInfos(TenantId tenantId, PageLink pageLink, String transportType);
DeviceProfile findDeviceProfileByCertificateHash(String credentialsId);
DeviceProfile findOrCreateDeviceProfile(TenantId tenantId, String profileName);
DeviceProfile createDefaultDeviceProfile(TenantId tenantId);

5
common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java

@ -66,6 +66,10 @@ public class DeviceProfile extends SearchTextBased<DeviceProfileId> implements H
private DeviceTransportType transportType;
@ApiModelProperty(position = 15, value = "Provisioning strategy.")
private DeviceProfileProvisionType provisionType;
@ApiModelProperty(position = 18, value = "CA certificate hash. ")
private String certificateHash;
@ApiModelProperty(position = 7, value = "Reference to the rule chain. " +
"If present, the specified rule chain will be used to process all messages related to device, including telemetry, attribute updates, etc. " +
"Otherwise, the root rule chain will be used to process those messages.")
@ -121,6 +125,7 @@ public class DeviceProfile extends SearchTextBased<DeviceProfileId> implements H
this.firmwareId = deviceProfile.getFirmwareId();
this.softwareId = deviceProfile.getSoftwareId();
this.defaultEdgeRuleChainId = deviceProfile.getDefaultEdgeRuleChainId();
this.certificateHash = deviceProfile.getCertificateHash();
this.externalId = deviceProfile.getExternalId();
}

3
common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileProvisionType.java

@ -18,5 +18,6 @@ package org.thingsboard.server.common.data;
public enum DeviceProfileProvisionType {
DISABLED,
ALLOW_CREATE_NEW_DEVICES,
CHECK_PRE_PROVISIONED_DEVICES
CHECK_PRE_PROVISIONED_DEVICES,
X509_CERTIFICATE_CHAIN
}

3
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java

@ -31,7 +31,8 @@ import java.io.Serializable;
@JsonSubTypes({
@JsonSubTypes.Type(value = DisabledDeviceProfileProvisionConfiguration.class, name = "DISABLED"),
@JsonSubTypes.Type(value = AllowCreateNewDevicesDeviceProfileProvisionConfiguration.class, name = "ALLOW_CREATE_NEW_DEVICES"),
@JsonSubTypes.Type(value = CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.class, name = "CHECK_PRE_PROVISIONED_DEVICES")})
@JsonSubTypes.Type(value = CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.class, name = "CHECK_PRE_PROVISIONED_DEVICES"),
@JsonSubTypes.Type(value = X509CertificateChainProvisionConfiguration.class, name = "X509_CERTIFICATE_CHAIN")})
public interface DeviceProfileProvisionConfiguration extends Serializable {
String getProvisionDeviceSecret();

37
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/X509CertificateChainProvisionConfiguration.java

@ -0,0 +1,37 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.device.profile;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
@Data
@NoArgsConstructor
public class X509CertificateChainProvisionConfiguration implements DeviceProfileProvisionConfiguration {
private String provisionDeviceSecret;
private String certificateValue;
private String certificateRegExPattern;
private boolean allowCreateNewDevicesByX509Certificate;
@Override
public DeviceProfileProvisionType getType() {
return DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN;
}
}

7
common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java

@ -35,6 +35,13 @@ public class EncryptionUtil {
.replaceAll("-----END CERTIFICATE-----", "");
}
public static String certTrimNewLinesForChainInDeviceProfile(String input) {
return input.replaceAll("\n", "")
.replaceAll("\r", "")
.replaceAll("-----BEGIN CERTIFICATE-----", "-----BEGIN CERTIFICATE-----\n")
.replaceAll("-----END CERTIFICATE-----", "\n-----END CERTIFICATE-----\n");
}
public static String pubkTrimNewLines(String input) {
return input.replaceAll("-----BEGIN PUBLIC KEY-----", "")
.replaceAll("\n", "")

68
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java

@ -24,9 +24,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.msg.EncryptionUtil;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.transport.TransportService;
import org.thingsboard.server.common.transport.TransportServiceCallback;
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
@ -42,7 +41,6 @@ import javax.net.ssl.SSLEngine;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.concurrent.CountDownLatch;
@ -123,7 +121,7 @@ public class MqttSslHandlerProvider {
static class ThingsboardMqttX509TrustManager implements X509TrustManager {
private final X509TrustManager trustManager;
private TransportService transportService;
private final TransportService transportService;
ThingsboardMqttX509TrustManager(X509TrustManager trustManager, TransportService transportService) {
this.trustManager = trustManager;
@ -142,43 +140,37 @@ public class MqttSslHandlerProvider {
}
@Override
public void checkClientTrusted(X509Certificate[] chain,
String authType) throws CertificateException {
String credentialsBody = null;
for (X509Certificate cert : chain) {
try {
String strCert = SslUtil.getCertificateString(cert);
String sha3Hash = EncryptionUtil.getSha3Hash(strCert);
final String[] credentialsBodyHolder = new String[1];
CountDownLatch latch = new CountDownLatch(1);
transportService.process(DeviceTransportType.MQTT, TransportProtos.ValidateDeviceX509CertRequestMsg.newBuilder().setHash(sha3Hash).build(),
new TransportServiceCallback<ValidateDeviceCredentialsResponse>() {
@Override
public void onSuccess(ValidateDeviceCredentialsResponse msg) {
if (!StringUtils.isEmpty(msg.getCredentials())) {
credentialsBodyHolder[0] = msg.getCredentials();
}
latch.countDown();
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
String clientDeviceCertValue = SslUtil.getCertificateString(chain[0]);
final String[] credentialsBodyHolder = new String[1];
CountDownLatch latch = new CountDownLatch(1);
try {
String certificateChain = SslUtil.getCertificateChainString(chain);
transportService.process(DeviceTransportType.MQTT, TransportProtos.ValidateOrCreateDeviceX509CertRequestMsg
.newBuilder().setCertificateChain(certificateChain).build(),
new TransportServiceCallback<>() {
@Override
public void onSuccess(ValidateDeviceCredentialsResponse msg) {
if (!StringUtils.isEmpty(msg.getCredentials())) {
credentialsBodyHolder[0] = msg.getCredentials();
}
@Override
public void onError(Throwable e) {
log.error(e.getMessage(), e);
latch.countDown();
}
});
latch.await(10, TimeUnit.SECONDS);
if (strCert.equals(credentialsBodyHolder[0])) {
credentialsBody = credentialsBodyHolder[0];
break;
}
} catch (InterruptedException | CertificateEncodingException e) {
log.error(e.getMessage(), e);
latch.countDown();
}
@Override
public void onError(Throwable e) {
log.trace("Failed to process certificate chain: {}", certificateChain, e);
latch.countDown();
}
});
latch.await(10, TimeUnit.SECONDS);
if (!clientDeviceCertValue.equals(credentialsBodyHolder[0])) {
throw new CertificateException("Invalid Certificate's chain. Cannot find such device credentials.");
}
}
if (credentialsBody == null) {
throw new CertificateException("Invalid Device Certificate");
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
}

6
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java

@ -54,8 +54,9 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcRequestMs
import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ValidateBasicMqttCredRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceLwM2MCredentialsRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ValidateOrCreateDeviceX509CertRequestMsg;
import java.util.List;
import java.util.concurrent.ExecutorService;
@ -87,6 +88,9 @@ public interface TransportService {
void process(DeviceTransportType transportType, ValidateDeviceX509CertRequestMsg msg,
TransportServiceCallback<ValidateDeviceCredentialsResponse> callback);
void process(DeviceTransportType transportType, ValidateOrCreateDeviceX509CertRequestMsg msg,
TransportServiceCallback<ValidateDeviceCredentialsResponse> callback);
void process(ValidateDeviceLwM2MCredentialsRequestMsg msg,
TransportServiceCallback<ValidateDeviceCredentialsResponse> callback);

9
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java

@ -72,7 +72,6 @@ import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGateway
import org.thingsboard.server.common.transport.auth.TransportDeviceInfo;
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
import org.thingsboard.server.common.transport.limits.TransportRateLimitService;
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.common.transport.util.JsonUtils;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg;
@ -97,6 +96,7 @@ import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.provider.TbTransportQueueFactory;
import org.thingsboard.server.queue.scheduler.SchedulerComponent;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.queue.util.TbTransportComponent;
import javax.annotation.PostConstruct;
@ -434,6 +434,13 @@ public class DefaultTransportService implements TransportService {
doProcess(transportType, protoMsg, callback);
}
@Override
public void process(DeviceTransportType transportType, TransportProtos.ValidateOrCreateDeviceX509CertRequestMsg msg, TransportServiceCallback<ValidateDeviceCredentialsResponse> callback) {
log.trace("Processing msg: {}", msg);
TbProtoQueueMsg<TransportApiRequestMsg> protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setValidateOrCreateX509CertRequestMsg(msg).build());
doProcess(transportType, protoMsg, callback);
}
private void doProcess(DeviceTransportType transportType, TbProtoQueueMsg<TransportApiRequestMsg> protoMsg,
TransportServiceCallback<ValidateDeviceCredentialsResponse> callback) {
ListenableFuture<ValidateDeviceCredentialsResponse> response = Futures.transform(transportApiRequestTemplate.send(protoMsg), tmp -> {

28
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java

@ -16,11 +16,17 @@
package org.thingsboard.server.common.transport.util;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.asn1.x500.RDN;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x500.style.IETFUtils;
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
import org.springframework.util.Base64Utils;
import org.thingsboard.server.common.msg.EncryptionUtil;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
/**
* @author Valerii Sosliuk
@ -35,4 +41,26 @@ public class SslUtil {
throws CertificateEncodingException {
return EncryptionUtil.certTrimNewLines(Base64Utils.encodeToString(cert.getEncoded()));
}
public static String getCertificateChainString(Certificate[] chain)
throws CertificateEncodingException {
String begin = "-----BEGIN CERTIFICATE-----";
String end = "-----END CERTIFICATE-----";
StringBuilder stringBuilder = new StringBuilder();
for (Certificate cert: chain) {
stringBuilder.append(begin).append(EncryptionUtil.certTrimNewLines(Base64Utils.encodeToString(cert.getEncoded()))).append(end).append("\n");
}
return stringBuilder.toString();
}
public static String parseCommonName(X509Certificate certificate) {
X500Name x500name;
try {
x500name = new JcaX509CertificateHolder(certificate).getSubject();
} catch (CertificateEncodingException e) {
throw new RuntimeException(e);
}
RDN cn = x500name.getRDNs(BCStyle.CN)[0];
return IETFUtils.valueToString(cn.getFirst().getValue());
}
}

3
dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java

@ -16,15 +16,12 @@
package org.thingsboard.server.dao.device;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.SecurityMode;
import org.eclipse.leshan.core.util.SecurityUtil;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils;

22
dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCacheKey.java

@ -29,35 +29,45 @@ public class DeviceProfileCacheKey implements Serializable {
private final TenantId tenantId;
private final String name;
private final DeviceProfileId deviceProfileId;
private final String certificateHash;
private final boolean defaultProfile;
private DeviceProfileCacheKey(TenantId tenantId, String name, DeviceProfileId deviceProfileId, boolean defaultProfile) {
private DeviceProfileCacheKey(TenantId tenantId, String name, DeviceProfileId deviceProfileId, String certificateHash, boolean defaultProfile) {
this.tenantId = tenantId;
this.name = name;
this.deviceProfileId = deviceProfileId;
this.certificateHash = certificateHash;
this.defaultProfile = defaultProfile;
}
public static DeviceProfileCacheKey fromName(TenantId tenantId, String name) {
return new DeviceProfileCacheKey(tenantId, name, null, false);
return new DeviceProfileCacheKey(tenantId, name, null, null, false);
}
public static DeviceProfileCacheKey fromId(DeviceProfileId id) {
return new DeviceProfileCacheKey(null, null, id, false);
return new DeviceProfileCacheKey(null, null, id, null, false);
}
public static DeviceProfileCacheKey fromCertificateHash(String certificateHash) {
return new DeviceProfileCacheKey(null, null, null, certificateHash, false);
}
public static DeviceProfileCacheKey defaultProfile(TenantId tenantId) {
return new DeviceProfileCacheKey(tenantId, null, null, true);
return new DeviceProfileCacheKey(tenantId, null, null, null, true);
}
/**
* IMPORTANT: Method toString() has to return unique value, if you add additional field to this class, please also refactor toString().
*/
@Override
public String toString() {
if (deviceProfileId != null) {
return deviceProfileId.toString();
} else if (defaultProfile) {
return tenantId.toString();
} else {
return tenantId + "_" + name;
} else if (certificateHash != null) {
return certificateHash;
}
return tenantId + "_" + name;
}
}

2
dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java

@ -45,4 +45,6 @@ public interface DeviceProfileDao extends Dao<DeviceProfile>, ExportableEntityDa
DeviceProfile findByProvisionDeviceKey(String provisionDeviceKey);
DeviceProfile findByName(TenantId tenantId, String profileName);
DeviceProfile findByCertificateHash(String certificateHash);
}

1
dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileEvictEvent.java

@ -27,5 +27,6 @@ public class DeviceProfileEvictEvent {
private final String oldName;
private final DeviceProfileId deviceProfileId;
private final boolean defaultProfile;
private final String certificateHash;
}

76
dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java

@ -34,12 +34,14 @@ import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileCon
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration;
import org.thingsboard.server.common.data.device.profile.X509CertificateChainProvisionConfiguration;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.msg.EncryptionUtil;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.queue.QueueService;
@ -47,12 +49,19 @@ import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.service.Validator;
import java.io.ByteArrayInputStream;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validateString;
@Service("DeviceProfileDaoService")
@Slf4j
@ -61,6 +70,7 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
private static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
private static final String INCORRECT_DEVICE_PROFILE_ID = "Incorrect deviceProfileId ";
private static final String INCORRECT_DEVICE_PROFILE_NAME = "Incorrect deviceProfileName ";
private static final String INCORRECT_DEVICE_PROFILE_CREDENTIALS_HASH = "Incorrect deviceProfileCertificateHash ";
private static final String DEVICE_PROFILE_WITH_SUCH_NAME_ALREADY_EXISTS = "Device profile with such name already exists!";
@Autowired
@ -93,6 +103,9 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
if (StringUtils.isNotEmpty(event.getOldName()) && !event.getOldName().equals(event.getNewName())) {
keys.add(DeviceProfileCacheKey.fromName(event.getTenantId(), event.getOldName()));
}
if (event.getCertificateHash() != null) {
keys.add(DeviceProfileCacheKey.fromCertificateHash(event.getCertificateHash()));
}
cache.evict(keys);
}
@ -122,15 +135,24 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
@Override
public DeviceProfile saveDeviceProfile(DeviceProfile deviceProfile) {
log.trace("Executing saveDeviceProfile [{}]", deviceProfile);
X509CertificateChainProvisionConfiguration x509Configuration = new X509CertificateChainProvisionConfiguration();
if (deviceProfile.getProfileData().getProvisionConfiguration() instanceof X509CertificateChainProvisionConfiguration) {
x509Configuration = (X509CertificateChainProvisionConfiguration) deviceProfile.getProfileData().getProvisionConfiguration();
}
if (x509Configuration.getCertificateValue() != null) {
formatDeviceProfileCertificate(deviceProfile, x509Configuration);
}
DeviceProfile oldDeviceProfile = deviceProfileValidator.validate(deviceProfile, DeviceProfile::getTenantId);
DeviceProfile savedDeviceProfile;
try {
savedDeviceProfile = deviceProfileDao.saveAndFlush(deviceProfile.getTenantId(), deviceProfile);
publishEvictEvent(new DeviceProfileEvictEvent(savedDeviceProfile.getTenantId(), savedDeviceProfile.getName(),
oldDeviceProfile != null ? oldDeviceProfile.getName() : null, savedDeviceProfile.getId(), savedDeviceProfile.isDefault()));
oldDeviceProfile != null ? oldDeviceProfile.getName() : null, savedDeviceProfile.getId(), savedDeviceProfile.isDefault(),
deviceProfile.getCertificateHash() != null ? deviceProfile.getCertificateHash() : null));
} catch (Exception t) {
handleEvictEvent(new DeviceProfileEvictEvent(deviceProfile.getTenantId(), deviceProfile.getName(),
oldDeviceProfile != null ? oldDeviceProfile.getName() : null, null, deviceProfile.isDefault()));
oldDeviceProfile != null ? oldDeviceProfile.getName() : null, null, deviceProfile.isDefault(),
deviceProfile.getCertificateHash() != null ? deviceProfile.getCertificateHash() : null));
checkConstraintViolation(t,
Map.of("device_profile_name_unq_key", DEVICE_PROFILE_WITH_SUCH_NAME_ALREADY_EXISTS,
"device_provision_key_unq_key", "Device profile with such provision device key already exists!",
@ -170,7 +192,8 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
deleteEntityRelations(tenantId, deviceProfileId);
deviceProfileDao.removeById(tenantId, deviceProfileId.getId());
publishEvictEvent(new DeviceProfileEvictEvent(deviceProfile.getTenantId(), deviceProfile.getName(),
null, deviceProfile.getId(), deviceProfile.isDefault()));
null, deviceProfile.getId(), deviceProfile.isDefault(),
deviceProfile.getCertificateHash() != null ? deviceProfile.getCertificateHash() : null));
} catch (Exception t) {
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_device_profile")) {
@ -197,6 +220,14 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
return deviceProfileDao.findDeviceProfileInfos(tenantId, pageLink, transportType);
}
@Override
public DeviceProfile findDeviceProfileByCertificateHash(String certificateHash) {
log.trace("Executing findDeviceProfileIdByCredentialsId credentialId [{}]", certificateHash);
validateString(certificateHash, INCORRECT_DEVICE_PROFILE_CREDENTIALS_HASH + certificateHash);
return cache.getAndPutInTransaction(DeviceProfileCacheKey.fromCertificateHash(certificateHash),
() -> deviceProfileDao.findByCertificateHash(certificateHash), true);
}
@Override
public DeviceProfile findOrCreateDeviceProfile(TenantId tenantId, String name) {
log.trace("Executing findOrCreateDefaultDeviceProfile");
@ -268,14 +299,14 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
boolean changed = false;
if (previousDefaultDeviceProfile == null) {
deviceProfileDao.save(tenantId, deviceProfile);
publishEvictEvent(new DeviceProfileEvictEvent(deviceProfile.getTenantId(), deviceProfile.getName(), null, deviceProfile.getId(), true));
publishEvictEvent(new DeviceProfileEvictEvent(deviceProfile.getTenantId(), deviceProfile.getName(), null, deviceProfile.getId(), true, null));
changed = true;
} else if (!previousDefaultDeviceProfile.getId().equals(deviceProfile.getId())) {
previousDefaultDeviceProfile.setDefault(false);
deviceProfileDao.save(tenantId, previousDefaultDeviceProfile);
deviceProfileDao.save(tenantId, deviceProfile);
publishEvictEvent(new DeviceProfileEvictEvent(previousDefaultDeviceProfile.getTenantId(), previousDefaultDeviceProfile.getName(), null, previousDefaultDeviceProfile.getId(), false));
publishEvictEvent(new DeviceProfileEvictEvent(deviceProfile.getTenantId(), deviceProfile.getName(), null, deviceProfile.getId(), true));
publishEvictEvent(new DeviceProfileEvictEvent(previousDefaultDeviceProfile.getTenantId(), previousDefaultDeviceProfile.getName(), null, previousDefaultDeviceProfile.getId(), false, null));
publishEvictEvent(new DeviceProfileEvictEvent(deviceProfile.getTenantId(), deviceProfile.getName(), null, deviceProfile.getId(), true, null));
changed = true;
}
return changed;
@ -319,4 +350,37 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
profile.getDefaultDashboardId(), profile.getType(), profile.getTransportType());
}
private void formatDeviceProfileCertificate(DeviceProfile deviceProfile, X509CertificateChainProvisionConfiguration x509Configuration) {
String certificateValue = formatCertificateValue(x509Configuration.getCertificateValue());
String cert = regexCertificateChain(certificateValue);
String sha3Hash = EncryptionUtil.getSha3Hash(cert);
DeviceProfileData deviceProfileData = deviceProfile.getProfileData();
x509Configuration.setCertificateValue(certificateValue);
deviceProfileData.setProvisionConfiguration(x509Configuration);
deviceProfile.setProfileData(deviceProfileData);
deviceProfile.setCertificateHash(sha3Hash);
}
private String regexCertificateChain(String chain) {
String regex = "-----BEGIN CERTIFICATE-----\\s*.*?\\s*-----END CERTIFICATE-----";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(chain);
if (matcher.find()) {
return matcher.group(0);
}
return chain;
}
private String formatCertificateValue(String certificateValue) {
try {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
ByteArrayInputStream inputStream = new ByteArrayInputStream(certificateValue.getBytes());
Certificate[] certificates = cf.generateCertificates(inputStream).toArray(new Certificate[0]);
if (certificates.length > 1) {
return EncryptionUtil.certTrimNewLinesForChainInDeviceProfile(certificateValue);
}
} catch (CertificateException ignored) {}
return EncryptionUtil.certTrimNewLines(certificateValue);
}
}

1
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -194,6 +194,7 @@ public class ModelConstants {
public static final String DEVICE_PROFILE_PROVISION_DEVICE_KEY = "provision_device_key";
public static final String DEVICE_PROFILE_FIRMWARE_ID_PROPERTY = "firmware_id";
public static final String DEVICE_PROFILE_SOFTWARE_ID_PROPERTY = "software_id";
public static final String DEVICE_PROFILE_CERTIFICATE_HASH_PROPERTY = "certificate_hash";
public static final String DEVICE_PROFILE_DEFAULT_EDGE_RULE_CHAIN_ID_PROPERTY = "default_edge_rule_chain_id";
/**

5
dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java

@ -109,6 +109,9 @@ public final class DeviceProfileEntity extends BaseSqlEntity<DeviceProfile> impl
@Column(name = ModelConstants.EXTERNAL_ID_PROPERTY)
private UUID externalId;
@Column(name = ModelConstants.DEVICE_PROFILE_CERTIFICATE_HASH_PROPERTY)
private String certificateHash;
public DeviceProfileEntity() {
super();
}
@ -126,6 +129,7 @@ public final class DeviceProfileEntity extends BaseSqlEntity<DeviceProfile> impl
this.image = deviceProfile.getImage();
this.transportType = deviceProfile.getTransportType();
this.provisionType = deviceProfile.getProvisionType();
this.certificateHash = deviceProfile.getCertificateHash();
this.description = deviceProfile.getDescription();
this.isDefault = deviceProfile.isDefault();
this.profileData = JacksonUtil.convertValue(deviceProfile.getProfileData(), ObjectNode.class);
@ -188,6 +192,7 @@ public final class DeviceProfileEntity extends BaseSqlEntity<DeviceProfile> impl
deviceProfile.setDefaultDashboardId(new DashboardId(defaultDashboardId));
}
deviceProfile.setProvisionDeviceKey(provisionDeviceKey);
deviceProfile.setCertificateHash(certificateHash);
if (firmwareId != null) {
deviceProfile.setFirmwareId(new OtaPackageId(firmwareId));

57
dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java

@ -21,6 +21,7 @@ import org.eclipse.leshan.core.util.SecurityUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.springframework.util.Base64Utils;
import org.springframework.util.CollectionUtils;
import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.DeviceProfile;
@ -55,6 +56,19 @@ import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantService;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.PKIXParameters;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@ -118,6 +132,15 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator<D
if (deviceProfile.getProvisionType() == null) {
deviceProfile.setProvisionType(DeviceProfileProvisionType.DISABLED);
}
if (deviceProfile.getCertificateHash() != null) {
if (getRootCAFromJavaCacerts(deviceProfile.getCertificateHash())) {
throw new DataValidationException("Device profile certificate cannot be well known root CA!");
}
DeviceProfile existingDeviceProfileCertificate = deviceProfileDao.findByCertificateHash(deviceProfile.getCertificateHash());
if (existingDeviceProfileCertificate != null && !existingDeviceProfileCertificate.getId().equals(deviceProfile.getId())) {
throw new DataValidationException("Device profile with such certificate hash already exists!");
}
}
DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration();
transportConfiguration.validate();
if (transportConfiguration instanceof MqttDeviceProfileTransportConfiguration) {
@ -211,6 +234,15 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator<D
throw new DataValidationException(message);
}
}
if (deviceProfile.getCertificateHash() != null) {
if (getRootCAFromJavaCacerts(deviceProfile.getCertificateHash())) {
throw new DataValidationException("Device profile certificate cannot be well known root CA!");
}
DeviceProfile existingDeviceProfileCertificate = deviceProfileDao.findByCertificateHash(deviceProfile.getCertificateHash());
if (existingDeviceProfileCertificate != null && !existingDeviceProfileCertificate.getId().equals(old.getId())) {
throw new DataValidationException("Device profile with such certificate hash already exists!");
}
}
return old;
}
@ -363,4 +395,29 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator<D
break;
}
}
boolean getRootCAFromJavaCacerts(String deviceProfileHash) {
try {
String filename = System.getProperty("java.home") + "/lib/security/cacerts".replace('/', File.separatorChar);
FileInputStream is = new FileInputStream(filename);
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
String password = "changeit";
keystore.load(is, password.toCharArray());
PKIXParameters params = new PKIXParameters(keystore);
for (TrustAnchor ta : params.getTrustAnchors()) {
X509Certificate cert = ta.getTrustedCert();
if (EncryptionUtil.getSha3Hash(getCertificateString(cert)).equals(deviceProfileHash)) {
return true;
}
}
} catch (CertificateException | KeyStoreException | NoSuchAlgorithmException |
InvalidAlgorithmParameterException | IOException ignored) {
}
return false;
}
private String getCertificateString(Certificate cert) throws CertificateEncodingException {
return EncryptionUtil.certTrimNewLines(Base64Utils.encodeToString(cert.getEncoded()));
}
}

2
dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java

@ -64,6 +64,8 @@ public interface DeviceProfileRepository extends JpaRepository<DeviceProfileEnti
"WHERE d.tenantId = :tenantId AND d.isDefault = true")
DeviceProfileInfo findDefaultDeviceProfileInfo(@Param("tenantId") UUID tenantId);
DeviceProfileEntity findDeviceProfileByCertificateHash(String certificateHash);
DeviceProfileEntity findByTenantIdAndName(UUID id, String profileName);
DeviceProfileEntity findByProvisionDeviceKey(@Param("provisionDeviceKey") String provisionDeviceKey);

5
dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java

@ -115,6 +115,11 @@ public class JpaDeviceProfileDao extends JpaAbstractSearchTextDao<DeviceProfileE
return DaoUtil.getData(deviceProfileRepository.findByTenantIdAndName(tenantId.getId(), profileName));
}
@Override
public DeviceProfile findByCertificateHash(String certificateHash) {
return DaoUtil.getData(deviceProfileRepository.findDeviceProfileByCertificateHash(certificateHash));
}
@Override
public DeviceProfile findByTenantIdAndExternalId(UUID tenantId, UUID externalId) {
return DaoUtil.getData(deviceProfileRepository.findByTenantIdAndExternalId(tenantId, externalId));

4
dao/src/main/resources/sql/schema-entities.sql

@ -295,8 +295,10 @@ CREATE TABLE IF NOT EXISTS device_profile (
default_dashboard_id uuid,
default_queue_name varchar(255),
provision_device_key varchar,
certificate_hash varchar,
default_edge_rule_chain_id uuid,
external_id uuid,
CONSTRAINT device_profile_credentials_hash_unq_key UNIQUE (certificate_hash),
CONSTRAINT device_profile_name_unq_key UNIQUE (tenant_id, name),
CONSTRAINT device_provision_key_unq_key UNIQUE (provision_device_key),
CONSTRAINT device_profile_external_id_unq_key UNIQUE (tenant_id, external_id),
@ -1034,4 +1036,4 @@ BEGIN
SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id;
RETURN json_build_object('success', true, 'modified', modified, 'alarm', row_to_json(result))::text;
END
$$;
$$;

Loading…
Cancel
Save