Merge branch 'feature/x509-device-cert-impr' of github.com:AndriiLandiak/thingsboard into feature/x509-device-provisioning

This commit is contained in:
Andrii Shvaika 2023-04-12 12:47:41 +03:00
commit 4cea4362dc
34 changed files with 887 additions and 143 deletions

View File

@ -20,15 +20,16 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cluster.TbClusterService;
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.StringUtils;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.device.profile.X509CertificateChainProvisionConfiguration;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
@ -36,15 +37,16 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.transport.util.SslUtil;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceDao;
import org.thingsboard.server.dao.device.DeviceProfileDao;
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;
@ -59,12 +61,13 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.state.DeviceStateService;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Service
@ -77,39 +80,28 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
private static final String DEVICE_PROVISION_STATE = "provisionState";
private static final String PROVISIONED_STATE = "provisioned";
@Autowired
TbClusterService clusterService;
private final TbClusterService clusterService;
private final DeviceProfileService deviceProfileService;
private final DeviceService deviceService;
private final DeviceCredentialsService deviceCredentialsService;
private final AttributesService attributesService;
private final AuditLogService auditLogService;
private final PartitionService partitionService;
@Autowired
DeviceDao deviceDao;
@Autowired
DeviceProfileDao deviceProfileDao;
@Autowired
DeviceService deviceService;
@Autowired
DeviceCredentialsService deviceCredentialsService;
@Autowired
AttributesService attributesService;
@Autowired
DeviceStateService deviceStateService;
@Autowired
AuditLogService auditLogService;
@Autowired
PartitionService partitionService;
public DeviceProvisionServiceImpl(TbQueueProducerProvider producerProvider) {
public DeviceProvisionServiceImpl(TbQueueProducerProvider producerProvider, TbClusterService clusterService, DeviceProfileService deviceProfileService, DeviceService deviceService, DeviceCredentialsService deviceCredentialsService, AttributesService attributesService, AuditLogService auditLogService, PartitionService partitionService) {
ruleEngineMsgProducer = producerProvider.getRuleEngineMsgProducer();
this.clusterService = clusterService;
this.deviceProfileService = deviceProfileService;
this.deviceService = deviceService;
this.deviceCredentialsService = deviceCredentialsService;
this.attributesService = attributesService;
this.auditLogService = auditLogService;
this.partitionService = partitionService;
}
@Override
public ProvisionResponse provisionDevice(ProvisionRequest provisionRequest) {
fetchAndApplyDeviceNameForX509ProvisionRequestWithRegEx(provisionRequest);
String provisionRequestKey = provisionRequest.getCredentials().getProvisionDeviceKey();
String provisionRequestSecret = provisionRequest.getCredentials().getProvisionDeviceSecret();
if (!StringUtils.isEmpty(provisionRequest.getDeviceName())) {
@ -124,14 +116,14 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name());
}
DeviceProfile targetProfile = deviceProfileDao.findByProvisionDeviceKey(provisionRequestKey);
DeviceProfile targetProfile = deviceProfileService.findDeviceProfileByProvisionDeviceKey(provisionRequestKey);
if (targetProfile == null || targetProfile.getProfileData().getProvisionConfiguration() == null ||
targetProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret() == null) {
throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name());
}
Device targetDevice = deviceDao.findDeviceByTenantIdAndName(targetProfile.getTenantId().getId(), provisionRequest.getDeviceName()).orElse(null);
Device targetDevice = deviceService.findDeviceByTenantIdAndName(targetProfile.getTenantId(), provisionRequest.getDeviceName());
switch (targetProfile.getProvisionType()) {
case ALLOW_CREATE_NEW_DEVICES:
@ -155,6 +147,25 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
}
}
break;
case X509_CERTIFICATE_CHAIN:
if (targetProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret().equals(provisionRequestSecret)) {
X509CertificateChainProvisionConfiguration x509Configuration = (X509CertificateChainProvisionConfiguration) targetProfile.getProfileData().getProvisionConfiguration();
if (targetDevice != null && targetDevice.getDeviceProfileId().equals(targetProfile.getId())) {
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(targetDevice.getTenantId(), targetDevice.getId());
if (deviceCredentials.getCredentialsType() == DeviceCredentialsType.X509_CERTIFICATE) {
String updatedDeviceCertificateValue = provisionRequest.getCredentialsData().getX509CertHash();
deviceCredentials = updateDeviceCredentials(targetDevice.getTenantId(), deviceCredentials,
updatedDeviceCertificateValue, DeviceCredentialsType.X509_CERTIFICATE);
}
return new ProvisionResponse(deviceCredentials, ProvisionResponseStatus.SUCCESS);
} else if (x509Configuration.isAllowCreateNewDevicesByX509Certificate()) {
return createDevice(provisionRequest, targetProfile);
} else {
log.warn("Device with name {} doesn't exist and cannot be created due incorrect configuration for X509CertificateChainProvisionConfiguration", provisionRequest.getDeviceName());
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
}
}
break;
}
throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name());
}
@ -209,6 +220,14 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
}
}
private DeviceCredentials updateDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials, String certificateValue,
DeviceCredentialsType credentialsType) {
log.trace("Updating device credentials [{}] with certificate value [{}]", deviceCredentials, certificateValue);
deviceCredentials.setCredentialsValue(certificateValue);
deviceCredentials.setCredentialsType(credentialsType);
return deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials);
}
private ListenableFuture<List<String>> saveProvisionStateAttribute(Device device) {
return attributesService.save(device.getTenantId(), device.getId(), DataConstants.SERVER_SCOPE,
Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry(DEVICE_PROVISION_STATE, PROVISIONED_STATE),
@ -257,4 +276,32 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
ActionType actionType = success ? ActionType.PROVISION_SUCCESS : ActionType.PROVISION_FAILURE;
auditLogService.logEntityAction(tenantId, customerId, new UserId(UserId.NULL_UUID), device.getName(), device.getId(), device, actionType, null, provisionRequest);
}
private void fetchAndApplyDeviceNameForX509ProvisionRequestWithRegEx(ProvisionRequest provisionRequest) {
DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileByProvisionDeviceKey(provisionRequest.getCredentials().getProvisionDeviceKey());
if (deviceProfile != null && DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN.equals(deviceProfile.getProfileData().getProvisionConfiguration().getType())) {
X509CertificateChainProvisionConfiguration configuration = (X509CertificateChainProvisionConfiguration) deviceProfile.getProfileData().getProvisionConfiguration();
String certificateValue = provisionRequest.getCredentialsData().getX509CertHash();
String certificateRegEx = configuration.getCertificateRegExPattern();
String deviceName = extractDeviceNameFromCertificateCNByRegEx(certificateValue, certificateRegEx);
if (deviceName == null) {
log.warn("Device name cannot be extracted using regex [{}] for certificate [{}]",certificateRegEx, certificateValue);
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
}
provisionRequest.setDeviceName(deviceName);
}
}
private String extractDeviceNameFromCertificateCNByRegEx(String x509Value, String regex) {
try {
String commonName = SslUtil.parseCommonName(SslUtil.readCertFile(x509Value));
log.trace("Extract CN [{}] by regex pattern [{}]", commonName, regex);
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(commonName);
if (matcher.find()) {
return matcher.group(0);
}
} catch (Exception ignored) {}
return null;
}
}

View File

@ -82,6 +82,9 @@ public class DefaultCacheCleanupService implements CacheCleanupService {
log.info("Clearing cache to upgrade from version 3.4.2 to 3.4.3 ...");
clearCacheByName("repositorySettings");
break;
case "3.4.4":
log.info("Clearing cache to upgrade from version 3.4.4 to 3.5.0");
clearCacheByName("deviceProfiles");
default:
//Do nothing, since cache cleanup is optional.
}

View File

@ -66,11 +66,13 @@ 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.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;
import org.thingsboard.server.dao.device.provision.ProvisionRequest;
import org.thingsboard.server.dao.device.provision.ProvisionResponse;
import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus;
import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.relation.RelationService;
@ -108,8 +110,10 @@ 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.Pattern;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN;
import static org.thingsboard.server.service.transport.BasicCredentialsValidationResult.PASSWORD_MISMATCH;
import static org.thingsboard.server.service.transport.BasicCredentialsValidationResult.VALID;
@ -124,10 +128,13 @@ public class DefaultTransportApiService implements TransportApiService {
private static final ObjectMapper mapper = new ObjectMapper();
private static final Pattern X509_CERTIFICATE_TRIM_CHAIN_PATTERN = Pattern.compile("-----BEGIN CERTIFICATE-----\\s*.*?\\s*-----END CERTIFICATE-----");
private final TbDeviceProfileCache deviceProfileCache;
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 +166,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());
} else if (transportApiRequestMsg.hasGetOrCreateDeviceRequestMsg()) {
result = handle(transportApiRequestMsg.getGetOrCreateDeviceRequestMsg());
} else if (transportApiRequestMsg.hasEntityProfileRequestMsg()) {
@ -226,6 +236,32 @@ public class DefaultTransportApiService implements TransportApiService {
}
}
protected ListenableFuture<TransportApiResponseMsg> validateOrCreateDeviceX509Certificate(String certificateChain) {
List<String> chain = X509_CERTIFICATE_TRIM_CHAIN_PATTERN.matcher(certificateChain).results().map(match ->
EncryptionUtil.certTrimNewLines(match.group())).collect(Collectors.toList());
for (String certificateValue : chain) {
String certificateHash = EncryptionUtil.getSha3Hash(certificateValue);
DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(certificateHash);
if (credentials != null && credentials.getCredentialsType() == DeviceCredentialsType.X509_CERTIFICATE) {
return getDeviceInfo(credentials);
}
DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileByProvisionDeviceKey(certificateHash);
if (deviceProfile != null && deviceProfile.getProvisionType() == X509_CERTIFICATE_CHAIN) {
String updatedDeviceProvisionSecret = chain.get(0);
ProvisionRequest provisionRequest = createProvisionRequest(deviceProfile, updatedDeviceProvisionSecret);
ProvisionResponse provisionResponse = provisionDeviceRequestAndGetResponse(provisionRequest);
if (provisionResponse != null && ProvisionResponseStatus.SUCCESS.equals(provisionResponse.getResponseStatus())) {
return getDeviceInfo(provisionResponse.getDeviceCredentials());
} else {
return getEmptyTransportApiResponseFuture();
}
} else if (deviceProfile != null) {
log.warn("[{}] Device Profile provision configuration mismatched: expected {}, actual {}", deviceProfile.getId(), X509_CERTIFICATE_CHAIN, deviceProfile.getProvisionType());
}
}
return getEmptyTransportApiResponseFuture();
}
private ListenableFuture<TransportApiResponseMsg> validateUserNameCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg mqtt) {
DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(mqtt.getUserName());
if (credentials != null) {
@ -665,4 +701,24 @@ public class DefaultTransportApiService implements TransportApiService {
private Long checkLong(Long l) {
return l != null ? l : 0;
}
private ProvisionRequest createProvisionRequest(DeviceProfile deviceProfile, String certificateValue) {
ProvisionDeviceProfileCredentials provisionDeviceProfileCredentials = new ProvisionDeviceProfileCredentials(
deviceProfile.getProvisionDeviceKey(),
deviceProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret()
);
ProvisionDeviceCredentialsData provisionDeviceCredentialsData = new ProvisionDeviceCredentialsData(null, null, null, null, certificateValue);
return new ProvisionRequest(null, DeviceCredentialsType.X509_CERTIFICATE, provisionDeviceCredentialsData, provisionDeviceProfileCredentials);
}
private ProvisionResponse provisionDeviceRequestAndGetResponse(ProvisionRequest provisionRequest) {
try {
return deviceProvisionService.provisionDevice(provisionRequest);
} catch (ProvisionFailedException e) {
log.error(e.getMessage());
}
return null;
}
}

View File

@ -130,6 +130,9 @@ security:
loginProcessingUrl: "${SECURITY_OAUTH2_LOGIN_PROCESSING_URL:/login/oauth2/code/}"
githubMapper:
emailUrl: "${SECURITY_OAUTH2_GITHUB_MAPPER_EMAIL_URL_KEY:https://api.github.com/user/emails}"
java_cacerts:
path: "${SECURITY_JAVA_CACERTS_PATH:${java.home}${file.separator}lib${file.separator}security${file.separator}cacerts}"
password: "${SECURITY_JAVA_CACERTS_PASSWORD:changeit}"
# Usage statistics parameters
usage:

View File

@ -299,6 +299,28 @@ 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.setProvisionDeviceKey("Certificate hash");
doPost("/api/deviceProfile", deviceProfile)
.andExpect(status().isOk());
DeviceProfile deviceProfile2 = this.createDeviceProfile("Device Profile 2");
deviceProfile2.setProvisionDeviceKey("Certificate hash");
Mockito.reset(tbClusterService, auditLogService);
String msgError = "Device profile with such provision device key 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");

View File

@ -0,0 +1,210 @@
/**
* 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.Before;
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.device.provision.ProvisionResponse;
import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus;
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.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
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 String certificateChain;
private String[] chain;
@Before
public void setUp() {
String filePath = "src/test/resources/mqtt/x509ChainProvisionTest.pem";
try {
certificateChain = Files.readString(Paths.get(filePath));
certificateChain = certTrimNewLinesForChainInDeviceProfile(certificateChain);
chain = fetchLeafCertificateFromChain(certificateChain);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Test
public void validateExistingDeviceX509Certificate() {
var device = createDevice();
when(deviceService.findDeviceByIdAsync(any(), any())).thenReturn(Futures.immediateFuture(device));
var deviceCredentials = createDeviceCredentials(chain[0], device.getId());
when(deviceCredentialsService.findDeviceCredentialsByCredentialsId(any())).thenReturn(deviceCredentials);
service.validateOrCreateDeviceX509Certificate(certificateChain);
verify(deviceCredentialsService, times(1)).findDeviceCredentialsByCredentialsId(any());
}
@Test
public void provisionDeviceX509Certificate() {
var deviceProfile = createDeviceProfile(chain[1]);
when(deviceProfileService.findDeviceProfileByProvisionDeviceKey(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(chain[0], device.getId());
when(deviceCredentialsService.findDeviceCredentialsByCredentialsId(any())).thenReturn(null);
when(deviceCredentialsService.updateDeviceCredentials(any(), any())).thenReturn(deviceCredentials);
var provisionResponse = createProvisionResponse(deviceCredentials);
when(deviceProvisionService.provisionDevice(any())).thenReturn(provisionResponse);
service.validateOrCreateDeviceX509Certificate(certificateChain);
verify(deviceProfileService, times(1)).findDeviceProfileByProvisionDeviceKey(any());
verify(deviceService, times(1)).findDeviceByIdAsync(any(), any());
verify(deviceCredentialsService, times(1)).findDeviceCredentialsByCredentialsId(any());
verify(deviceProvisionService, times(1)).provisionDevice(any());
}
private DeviceProfile createDeviceProfile(String certificateValue) {
X509CertificateChainProvisionConfiguration provision = new X509CertificateChainProvisionConfiguration();
provision.setProvisionDeviceSecret(certificateValue);
provision.setCertificateRegExPattern("([^@]+)");
provision.setAllowCreateNewDevicesByX509Certificate(true);
DeviceProfileData deviceProfileData = new DeviceProfileData();
deviceProfileData.setProvisionConfiguration(provision);
DeviceProfile deviceProfile = new DeviceProfile();
deviceProfile.setProfileData(deviceProfileData);
deviceProfile.setProvisionDeviceKey(EncryptionUtil.getSha3Hash(certificateValue));
deviceProfile.setProvisionType(DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN);
return deviceProfile;
}
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 Device createDevice() {
Device device = new Device();
device.setId(new DeviceId(UUID.randomUUID()));
return device;
}
private ProvisionResponse createProvisionResponse(DeviceCredentials deviceCredentials) {
return new ProvisionResponse(deviceCredentials, ProvisionResponseStatus.SUCCESS);
}
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")
.trim();
}
private String[] fetchLeafCertificateFromChain(String value) {
List<String> chain = new ArrayList<>();
String regex = "-----BEGIN CERTIFICATE-----\\s*.*?\\s*-----END CERTIFICATE-----";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(value);
while (matcher.find()) {
chain.add(matcher.group(0));
}
return chain.toArray(new String[0]);
}
}

View File

@ -0,0 +1,28 @@
-----BEGIN CERTIFICATE-----
MIICMTCCAdegAwIBAgIUI9dBuwN6pTtK6uZ03rkiCwV4wEYwCgYIKoZIzj0EAwIw
bjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGlu
Z3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlQ2VydGlmaWNhdGVAWDUwOVBy
b3Zpc2lvblN0cmF0ZWd5MB4XDTIzMDMyOTE0NTYxN1oXDTI0MDMyODE0NTYxN1ow
bjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGlu
Z3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlQ2VydGlmaWNhdGVAWDUwOVBy
b3Zpc2lvblN0cmF0ZWd5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9Zo791qK
QiGNBm11r4ZGxh+w+ossZL3xc46ufq5QckQHP7zkD2XDAcmP5GvdkM1sBFN9AWaC
kQfNnWmfERsOOKNTMFEwHQYDVR0OBBYEFFFc5uyCyglQoZiKhzXzMcQ3BKORMB8G
A1UdIwQYMBaAFFFc5uyCyglQoZiKhzXzMcQ3BKORMA8GA1UdEwEB/wQFMAMBAf8w
CgYIKoZIzj0EAwIDSAAwRQIhANbA9CuhoOifZMMmqkpuld+65CR+ItKdXeRAhLMZ
uccuAiB0FSQB34zMutXrZj1g8Gl5OkE7YryFHbei1z0SveHR8g==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICMTCCAdegAwIBAgIUUEKxS9hTz4l+oLUMF0LV6TC/gCIwCgYIKoZIzj0EAwIw
bjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGlu
Z3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlUHJvZmlsZUNlcnRAWDUwOVBy
b3Zpc2lvblN0cmF0ZWd5MB4XDTIzMDMyOTE0NTczNloXDTI0MDMyODE0NTczNlow
bjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGlu
Z3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlUHJvZmlsZUNlcnRAWDUwOVBy
b3Zpc2lvblN0cmF0ZWd5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAECMlWO72k
rDoUL9FQjUmSCetkhaEGJUfQkdSfkLSNa0GyAEIMbfmzI4zITeapunu4rGet3EMy
LydQzuQanBicp6NTMFEwHQYDVR0OBBYEFHpZ78tPnztNii4Da/yCw6mhEIL3MB8G
A1UdIwQYMBaAFHpZ78tPnztNii4Da/yCw6mhEIL3MA8GA1UdEwEB/wQFMAMBAf8w
CgYIKoZIzj0EAwIDSAAwRQIgJ7qyMFqNcwSYkH6o+UlQXzLWfwZbNjVk+aR7foAZ
NGsCIQDsd7v3WQIGHiArfZeDs1DLEDuV/2h6L+ZNoGNhEKL+1A==
-----END CERTIFICATE-----

View File

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

View File

@ -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 {

View File

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

View File

@ -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
}

View File

@ -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();

View File

@ -0,0 +1,36 @@
/**
* 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 certificateRegExPattern;
private boolean allowCreateNewDevicesByX509Certificate;
@Override
public DeviceProfileProvisionType getType() {
return DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN;
}
}

View File

@ -35,6 +35,14 @@ 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")
.trim();
}
public static String pubkTrimNewLines(String input) {
return input.replaceAll("-----BEGIN PUBLIC KEY-----", "")
.replaceAll("\n", "")

View File

@ -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,17 +140,15 @@ 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);
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);
transportService.process(DeviceTransportType.MQTT, TransportProtos.ValidateDeviceX509CertRequestMsg.newBuilder().setHash(sha3Hash).build(),
new TransportServiceCallback<ValidateDeviceCredentialsResponse>() {
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())) {
@ -163,23 +159,22 @@ public class MqttSslHandlerProvider {
@Override
public void onError(Throwable e) {
log.error(e.getMessage(), e);
log.trace("Failed to process certificate chain: {}", certificateChain, e);
latch.countDown();
}
});
latch.await(10, TimeUnit.SECONDS);
if (strCert.equals(credentialsBodyHolder[0])) {
credentialsBody = credentialsBodyHolder[0];
break;
if (!clientDeviceCertValue.equals(credentialsBodyHolder[0])) {
log.debug("Failed to find credentials for device certificate chain: {}", chain);
if (chain.length == 1) {
throw new CertificateException("Invalid Device Certificate");
} else {
throw new CertificateException("Invalid Chain of X509 Certificates");
}
} catch (InterruptedException | CertificateEncodingException e) {
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
if (credentialsBody == null) {
log.debug("Failed to find credentials for device certificate chain: {}", chain);
throw new CertificateException("Invalid Device Certificate");
}
}
}
}

View File

@ -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);

View File

@ -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 -> {

View File

@ -16,11 +16,21 @@
package org.thingsboard.server.common.transport.util;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
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.io.ByteArrayInputStream;
import java.io.InputStream;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
/**
* @author Valerii Sosliuk
@ -35,4 +45,44 @@ 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 X509Certificate readCertFile(String fileContent) {
X509Certificate certificate = null;
try {
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);
}
}
} catch (Exception ignored) {}
return certificate;
}
public static String parseCommonName(X509Certificate certificate) {
X500Name x500name;
try {
x500name = new JcaX509CertificateHolder(certificate).getSubject();
} catch (CertificateEncodingException e) {
log.warn("Cannot parse CN from device certificate");
throw new RuntimeException(e);
}
RDN cn = x500name.getRDNs(BCStyle.CN)[0];
return IETFUtils.valueToString(cn.getFirst().getValue());
}
}

View File

@ -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;

View File

@ -16,6 +16,7 @@
package org.thingsboard.server.dao.device;
import lombok.Data;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId;
@ -30,34 +31,44 @@ public class DeviceProfileCacheKey implements Serializable {
private final String name;
private final DeviceProfileId deviceProfileId;
private final boolean defaultProfile;
private final String provisionDeviceKey;
private DeviceProfileCacheKey(TenantId tenantId, String name, DeviceProfileId deviceProfileId, boolean defaultProfile) {
private DeviceProfileCacheKey(TenantId tenantId, String name, DeviceProfileId deviceProfileId, boolean defaultProfile, String provisionDeviceKey) {
this.tenantId = tenantId;
this.name = name;
this.deviceProfileId = deviceProfileId;
this.defaultProfile = defaultProfile;
this.provisionDeviceKey = provisionDeviceKey;
}
public static DeviceProfileCacheKey fromName(TenantId tenantId, String name) {
return new DeviceProfileCacheKey(tenantId, name, null, false);
return new DeviceProfileCacheKey(tenantId, name, null, false, null);
}
public static DeviceProfileCacheKey fromId(DeviceProfileId id) {
return new DeviceProfileCacheKey(null, null, id, false);
return new DeviceProfileCacheKey(null, null, id, false, null);
}
public static DeviceProfileCacheKey defaultProfile(TenantId tenantId) {
return new DeviceProfileCacheKey(tenantId, null, null, true);
return new DeviceProfileCacheKey(tenantId, null, null, true, null);
}
public static DeviceProfileCacheKey fromProvisionDeviceKey(String provisionDeviceKey) {
return new DeviceProfileCacheKey(null, null, null, false, provisionDeviceKey);
}
/**
* 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 {
} else if (StringUtils.isNotEmpty(provisionDeviceKey)) {
return provisionDeviceKey;
}
return tenantId + "_" + name;
}
}
}

View File

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

View File

@ -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_PROVISION_DEVICE_KEY = "Incorrect provisionDeviceKey ";
private static final String DEVICE_PROFILE_WITH_SUCH_NAME_ALREADY_EXISTS = "Device profile with such name already exists!";
@Autowired
@ -93,13 +103,16 @@ 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 (StringUtils.isNotEmpty(event.getProvisionDeviceKey())) {
keys.add(DeviceProfileCacheKey.fromProvisionDeviceKey(event.getProvisionDeviceKey()));
}
cache.evict(keys);
}
@Override
public DeviceProfile findDeviceProfileById(TenantId tenantId, DeviceProfileId deviceProfileId) {
log.trace("Executing findDeviceProfileById [{}]", deviceProfileId);
Validator.validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId);
validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId);
return cache.getAndPutInTransaction(DeviceProfileCacheKey.fromId(deviceProfileId),
() -> deviceProfileDao.findById(tenantId, deviceProfileId.getId()), true);
}
@ -107,30 +120,46 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
@Override
public DeviceProfile findDeviceProfileByName(TenantId tenantId, String profileName) {
log.trace("Executing findDeviceProfileByName [{}][{}]", tenantId, profileName);
Validator.validateString(profileName, INCORRECT_DEVICE_PROFILE_NAME + profileName);
validateString(profileName, INCORRECT_DEVICE_PROFILE_NAME + profileName);
return cache.getAndPutInTransaction(DeviceProfileCacheKey.fromName(tenantId, profileName),
() -> deviceProfileDao.findByName(tenantId, profileName), true);
}
@Override
public DeviceProfile findDeviceProfileByProvisionDeviceKey(String provisionDeviceKey) {
log.trace("Executing findDeviceProfileByProvisionDeviceKey provisionKey [{}]", provisionDeviceKey);
validateString(provisionDeviceKey, INCORRECT_PROVISION_DEVICE_KEY + provisionDeviceKey);
return cache.getAndPutInTransaction(DeviceProfileCacheKey.fromProvisionDeviceKey(provisionDeviceKey),
() -> deviceProfileDao.findByProvisionDeviceKey(provisionDeviceKey), false);
}
@Override
public DeviceProfileInfo findDeviceProfileInfoById(TenantId tenantId, DeviceProfileId deviceProfileId) {
log.trace("Executing findDeviceProfileById [{}]", deviceProfileId);
Validator.validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId);
validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId);
return toDeviceProfileInfo(findDeviceProfileById(tenantId, deviceProfileId));
}
@Override
public DeviceProfile saveDeviceProfile(DeviceProfile deviceProfile) {
log.trace("Executing saveDeviceProfile [{}]", deviceProfile);
if (deviceProfile.getProfileData() != null && deviceProfile.getProfileData().getProvisionConfiguration() instanceof X509CertificateChainProvisionConfiguration) {
X509CertificateChainProvisionConfiguration x509Configuration = (X509CertificateChainProvisionConfiguration) deviceProfile.getProfileData().getProvisionConfiguration();
if (x509Configuration.getProvisionDeviceSecret() != 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(),
oldDeviceProfile != null ? oldDeviceProfile.getProvisionDeviceKey() : 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(),
oldDeviceProfile != null ? oldDeviceProfile.getProvisionDeviceKey() : 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!",
@ -156,7 +185,7 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
@Transactional
public void deleteDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId) {
log.trace("Executing deleteDeviceProfile [{}]", deviceProfileId);
Validator.validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId);
validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId);
DeviceProfile deviceProfile = deviceProfileDao.findById(tenantId, deviceProfileId.getId());
if (deviceProfile != null && deviceProfile.isDefault()) {
throw new DataValidationException("Deletion of Default Device Profile is prohibited!");
@ -170,7 +199,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.getProvisionDeviceKey()));
} catch (Exception t) {
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_device_profile")) {
@ -260,7 +290,7 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
@Override
public boolean setDefaultDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId) {
log.trace("Executing setDefaultDeviceProfile [{}]", deviceProfileId);
Validator.validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId);
validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId);
DeviceProfile deviceProfile = deviceProfileDao.findById(tenantId, deviceProfileId.getId());
if (!deviceProfile.isDefault()) {
deviceProfile.setDefault(true);
@ -268,14 +298,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, deviceProfile.getProvisionDeviceKey()));
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, deviceProfile.getProvisionDeviceKey()));
publishEvictEvent(new DeviceProfileEvictEvent(deviceProfile.getTenantId(), deviceProfile.getName(), null, deviceProfile.getId(), true, deviceProfile.getProvisionDeviceKey()));
changed = true;
}
return changed;
@ -319,4 +349,38 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
profile.getDefaultDashboardId(), profile.getType(), profile.getTransportType());
}
private void formatDeviceProfileCertificate(DeviceProfile deviceProfile, X509CertificateChainProvisionConfiguration x509Configuration) {
String formattedCertificateValue = formatCertificateValue(x509Configuration.getProvisionDeviceSecret());
String cert = fetchLeafCertificateFromChain(formattedCertificateValue);
String sha3Hash = EncryptionUtil.getSha3Hash(cert);
DeviceProfileData deviceProfileData = deviceProfile.getProfileData();
x509Configuration.setProvisionDeviceSecret(formattedCertificateValue);
deviceProfileData.setProvisionConfiguration(x509Configuration);
deviceProfile.setProfileData(deviceProfileData);
deviceProfile.setProvisionDeviceKey(sha3Hash);
}
private String fetchLeafCertificateFromChain(String value) {
String regex = "-----BEGIN CERTIFICATE-----\\s*.*?\\s*-----END CERTIFICATE-----";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(value);
if (matcher.find()) {
// if the method receives a chain it fetches the leaf (end-entity) certificate, else if it gets a single certificate, it returns the single certificate
return matcher.group(0);
}
return value;
}
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);
}
}

View File

@ -19,8 +19,10 @@ import com.google.protobuf.Descriptors;
import com.google.protobuf.DynamicMessage;
import org.eclipse.leshan.core.util.SecurityUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
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 +57,17 @@ import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantService;
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.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;
@ -85,6 +98,12 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator<D
@Autowired
private DashboardService dashboardService;
@Value("${security.java_cacerts.path}")
private String javaCacertsPath;
@Value("${security.java_cacerts.password}")
private String javaCacertsPassword;
@Override
protected void validateDataImpl(TenantId tenantId, DeviceProfile deviceProfile) {
if (StringUtils.isEmpty(deviceProfile.getName())) {
@ -118,6 +137,11 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator<D
if (deviceProfile.getProvisionType() == null) {
deviceProfile.setProvisionType(DeviceProfileProvisionType.DISABLED);
}
if (deviceProfile.getProvisionDeviceKey() != null && DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN.equals(deviceProfile.getProvisionType())) {
if (isDeviceProfileCertificateInJavaCacerts(deviceProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret())) {
throw new DataValidationException("Device profile certificate cannot be well known root CA!");
}
}
DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration();
transportConfiguration.validate();
if (transportConfiguration instanceof MqttDeviceProfileTransportConfiguration) {
@ -211,6 +235,11 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator<D
throw new DataValidationException(message);
}
}
if (deviceProfile.getProvisionDeviceKey() != null && DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN.equals(deviceProfile.getProvisionType())) {
if (isDeviceProfileCertificateInJavaCacerts(deviceProfile.getProvisionDeviceKey())) {
throw new DataValidationException("Device profile certificate cannot be well known root CA!");
}
}
return old;
}
@ -363,4 +392,27 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator<D
break;
}
}
private boolean isDeviceProfileCertificateInJavaCacerts(String deviceProfileX509Secret) {
try {
FileInputStream is = new FileInputStream(javaCacertsPath);
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
keystore.load(is, javaCacertsPassword.toCharArray());
PKIXParameters params = new PKIXParameters(keystore);
for (TrustAnchor ta : params.getTrustAnchors()) {
X509Certificate cert = ta.getTrustedCert();
if (getCertificateString(cert).equals(deviceProfileX509Secret)) {
return true;
}
}
} catch (CertificateException | KeyStoreException | NoSuchAlgorithmException |
InvalidAlgorithmParameterException | IOException ignored) {
}
return false;
}
private String getCertificateString(X509Certificate cert) throws CertificateEncodingException {
return EncryptionUtil.certTrimNewLines(Base64Utils.encodeToString(cert.getEncoded()));
}
}

View File

@ -82,6 +82,8 @@ redis.connection.password=
security.user_login_case_sensitive=true
security.claim.allowClaimingByDefault=true
security.claim.duration=60000
security.java_cacerts.path=/path/to/cacerts/file
security.java_cacerts.password=myPassword
database.ts_max_intervals=700

View File

@ -27,7 +27,49 @@
{{ 'device-profile.provision-strategy-required' | translate }}
</mat-error>
</mat-form-field>
<section *ngIf="provisionConfigurationFormGroup.get('type').value !== deviceProvisionType.DISABLED" fxLayoutGap.gt-xs="8px" fxLayout="row" fxLayout.xs="column">
<div [ngSwitch]="provisionConfigurationFormGroup.get('type').value">
<ng-template [ngSwitchCase]="deviceProvisionType.ALLOW_CREATE_NEW_DEVICES">
<ng-container *ngTemplateOutlet="default"></ng-container>
</ng-template>
<ng-template [ngSwitchCase]="deviceProvisionType.CHECK_PRE_PROVISIONED_DEVICES">
<ng-container *ngTemplateOutlet="default"></ng-container>
</ng-template>
<ng-template [ngSwitchCase]="deviceProvisionType.X509_CERTIFICATE_CHAIN">
<div fxFlex fxLayoutAlign="start center" class="tb-hint">
<span [innerHTML]="'device-profile.provision-strategy-x509.certificate-chain-hint' | translate"></span>
<span tb-help-popup="device-profile/x509-chain-hint"
tb-help-popup-placement="top"
trigger-style="letter-spacing:0.25px; font-size: 12px"
[tb-help-popup-style]="{maxWidth: '820px'}"
trigger-text="{{ 'action.more' | translate }}"></span>
</div>
<mat-slide-toggle formControlName="allowCreateNewDevicesByX509Certificate">
{{ 'device-profile.provision-strategy-x509.allow-create-new-devices' | translate }}
</mat-slide-toggle>
<div class="tb-hint" style="padding:0 40px 16px" [innerHTML]="'device-profile.provision-strategy-x509.allow-create-new-devices-hint' | translate"></div>
<mat-form-field class="mat-block">
<mat-label translate>device-profile.provision-strategy-x509.certificate-value</mat-label>
<textarea matInput formControlName="certificateValue" cols="15" rows="5" required></textarea>
<mat-error *ngIf="provisionConfigurationFormGroup.get('certificateValue').hasError('required')">
{{ 'device-profile.provision-strategy-x509.certificate-value-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>device-profile.provision-strategy-x509.cn-regex-variable</mat-label>
<input matInput type="text" formControlName="certificateRegExPattern" required>
<span matSuffix
tb-help-popup="device-profile/x509-chain-regex-examples"
tb-help-popup-placement="top"
trigger-style="letter-spacing:0.25px"
[tb-help-popup-style]="{maxWidth: '820px'}"></span>
<mat-error *ngIf="provisionConfigurationFormGroup.get('certificateRegExPattern').hasError('required')">
{{ 'device-profile.provision-strategy-x509.cn-regex-variable-required' | translate }}
</mat-error>
<mat-hint translate>device-profile.provision-strategy-x509.cn-regex-variable-hint</mat-hint>
</mat-form-field>
</ng-template>
<ng-template #default>
<section fxLayoutGap.gt-xs="8px" fxLayout="row" fxLayout.xs="column">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>device-profile.provision-device-key</mat-label>
<input matInput formControlName="provisionDeviceKey" required/>
@ -59,4 +101,6 @@
</mat-error>
</mat-form-field>
</section>
</ng-template>
</div>
</div>

View File

@ -32,7 +32,7 @@ import {
DeviceProvisionType,
deviceProvisionTypeTranslationMap
} from '@shared/models/device.models';
import { generateSecret, isDefinedAndNotNull } from '@core/utils';
import { generateSecret, isBoolean, isDefinedAndNotNull } from '@core/utils';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -86,14 +86,36 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu
this.provisionConfigurationFormGroup = this.fb.group({
type: [DeviceProvisionType.DISABLED, Validators.required],
provisionDeviceSecret: [{value: null, disabled: true}, Validators.required],
provisionDeviceKey: [{value: null, disabled: true}, Validators.required]
provisionDeviceKey: [{value: null, disabled: true}, Validators.required],
certificateValue: [{value: null, disabled: true}, Validators.required],
certificateRegExPattern: [{value: null, disabled: true}, Validators.required],
allowCreateNewDevicesByX509Certificate: [{value: null, disabled: true}, Validators.required]
});
this.provisionConfigurationFormGroup.get('type').valueChanges.subscribe((type) => {
if (type === DeviceProvisionType.DISABLED) {
this.provisionConfigurationFormGroup.get('provisionDeviceSecret').disable({emitEvent: false});
this.provisionConfigurationFormGroup.get('provisionDeviceSecret').patchValue(null, {emitEvent: false});
this.provisionConfigurationFormGroup.get('provisionDeviceKey').disable({emitEvent: false});
this.provisionConfigurationFormGroup.get('provisionDeviceKey').patchValue(null);
for (const field in this.provisionConfigurationFormGroup.controls) {
if (field !== 'type') {
const control = this.provisionConfigurationFormGroup.get(field);
control.disable({emitEvent: false});
control.patchValue(null, {emitEvent: false});
}
}
} else if (type === DeviceProvisionType.X509_CERTIFICATE_CHAIN) {
const certificateValue: string = this.provisionConfigurationFormGroup.get('certificateValue').value;
if (!certificateValue || !certificateValue.length) {
this.provisionConfigurationFormGroup.get('certificateValue').patchValue(null, {emitEvent: false});
}
const certificateRegExPattern: string = this.provisionConfigurationFormGroup.get('certificateRegExPattern').value;
if (!certificateRegExPattern || !certificateRegExPattern.length) {
this.provisionConfigurationFormGroup.get('certificateRegExPattern').patchValue('[\\w]*', {emitEvent: false});
}
const allowCreateNewDevicesByX509Certificate: boolean | null = this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').value;
if (!isBoolean(allowCreateNewDevicesByX509Certificate)) {
this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').patchValue(true, {emitEvent: false});
}
this.provisionConfigurationFormGroup.get('certificateValue').enable({emitEvent: false});
this.provisionConfigurationFormGroup.get('certificateRegExPattern').enable({emitEvent: false});
this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').enable({emitEvent: false});
} else {
const provisionDeviceSecret: string = this.provisionConfigurationFormGroup.get('provisionDeviceSecret').value;
if (!provisionDeviceSecret || !provisionDeviceSecret.length) {
@ -121,6 +143,9 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu
writeValue(value: DeviceProvisionConfiguration | null): void {
if (isDefinedAndNotNull(value)) {
if (value.type === DeviceProvisionType.X509_CERTIFICATE_CHAIN) {
value.certificateValue = value.provisionDeviceSecret;
}
this.provisionConfigurationFormGroup.patchValue(value, {emitEvent: false});
} else {
this.provisionConfigurationFormGroup.patchValue({type: DeviceProvisionType.DISABLED});
@ -150,8 +175,12 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu
private updateModel(): void {
let deviceProvisionConfiguration: DeviceProvisionConfiguration = null;
this.resetFormControls(this.provisionConfigurationFormGroup.value);
if (this.provisionConfigurationFormGroup.valid) {
deviceProvisionConfiguration = this.provisionConfigurationFormGroup.getRawValue();
if (deviceProvisionConfiguration.type === DeviceProvisionType.X509_CERTIFICATE_CHAIN) {
deviceProvisionConfiguration.provisionDeviceSecret = deviceProvisionConfiguration.certificateValue;
}
}
this.propagateChange(deviceProvisionConfiguration);
}
@ -166,4 +195,15 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu
horizontalPosition: 'right'
}));
}
private resetFormControls(value: DeviceProvisionConfiguration) {
if (value.type === DeviceProvisionType.CHECK_PRE_PROVISIONED_DEVICES || value.type === DeviceProvisionType.ALLOW_CREATE_NEW_DEVICES) {
this.provisionConfigurationFormGroup.get('certificateValue').reset({value: null, disabled: true}, {emitEvent: false});
this.provisionConfigurationFormGroup.get('certificateRegExPattern').reset({value: null, disabled: true}, {emitEvent: false});
this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').reset({value: null, disabled: true}, {emitEvent: false});
} else if (value.type === DeviceProvisionType.X509_CERTIFICATE_CHAIN) {
this.provisionConfigurationFormGroup.get('provisionDeviceSecret').reset({value: null, disabled: true}, {emitEvent: false});
this.provisionConfigurationFormGroup.get('provisionDeviceKey').reset({value: null, disabled: true}, {emitEvent: false});
}
}
}

View File

@ -104,7 +104,9 @@ export class DeviceProfileComponent extends EntityComponent<DeviceProfile> {
const deviceProvisionConfiguration: DeviceProvisionConfiguration = {
type: entity?.provisionType ? entity.provisionType : DeviceProvisionType.DISABLED,
provisionDeviceKey: entity?.provisionDeviceKey,
provisionDeviceSecret: entity?.profileData?.provisionConfiguration?.provisionDeviceSecret
provisionDeviceSecret: entity?.profileData?.provisionConfiguration?.provisionDeviceSecret,
certificateRegExPattern: entity?.profileData?.provisionConfiguration?.certificateRegExPattern,
allowCreateNewDevicesByX509Certificate: entity?.profileData?.provisionConfiguration?.allowCreateNewDevicesByX509Certificate
};
const form = this.fb.group(
{
@ -185,7 +187,9 @@ export class DeviceProfileComponent extends EntityComponent<DeviceProfile> {
const deviceProvisionConfiguration: DeviceProvisionConfiguration = {
type: entity?.provisionType ? entity.provisionType : DeviceProvisionType.DISABLED,
provisionDeviceKey: entity?.provisionDeviceKey,
provisionDeviceSecret: entity?.profileData?.provisionConfiguration?.provisionDeviceSecret
provisionDeviceSecret: entity?.profileData?.provisionConfiguration?.provisionDeviceSecret,
certificateRegExPattern: entity?.profileData?.provisionConfiguration?.certificateRegExPattern,
allowCreateNewDevicesByX509Certificate: entity?.profileData?.provisionConfiguration?.allowCreateNewDevicesByX509Certificate
};
this.entityForm.patchValue({name: entity.name});
this.entityForm.patchValue({type: entity.type}, {emitEvent: false});

View File

@ -169,6 +169,6 @@
<mat-checkbox formControlName="sendAckOnValidationException">
{{ 'device-profile.mqtt-send-ack-on-validation-exception' | translate }}
</mat-checkbox>
<div class="tb-hint" innerHTML="{{ 'device-profile.mqtt-send-ack-on-validation-exception-hint' | translate }}"></div>
<div class="tb-hint" style="max-width: 800px" innerHTML="{{ 'device-profile.mqtt-send-ack-on-validation-exception-hint' | translate }}"></div>
</div>
</form>

View File

@ -62,7 +62,8 @@ export enum CoapTransportDeviceType {
export enum DeviceProvisionType {
DISABLED = 'DISABLED',
ALLOW_CREATE_NEW_DEVICES = 'ALLOW_CREATE_NEW_DEVICES',
CHECK_PRE_PROVISIONED_DEVICES = 'CHECK_PRE_PROVISIONED_DEVICES'
CHECK_PRE_PROVISIONED_DEVICES = 'CHECK_PRE_PROVISIONED_DEVICES',
X509_CERTIFICATE_CHAIN = 'X509_CERTIFICATE_CHAIN'
}
export interface DeviceConfigurationFormInfo {
@ -110,7 +111,8 @@ export const deviceProvisionTypeTranslationMap = new Map<DeviceProvisionType, st
[
[DeviceProvisionType.DISABLED, 'device-profile.provision-strategy-disabled'],
[DeviceProvisionType.ALLOW_CREATE_NEW_DEVICES, 'device-profile.provision-strategy-created-new'],
[DeviceProvisionType.CHECK_PRE_PROVISIONED_DEVICES, 'device-profile.provision-strategy-check-pre-provisioned']
[DeviceProvisionType.CHECK_PRE_PROVISIONED_DEVICES, 'device-profile.provision-strategy-check-pre-provisioned'],
[DeviceProvisionType.X509_CERTIFICATE_CHAIN, 'device-profile.provision-strategy-x509.certificate-chain']
]
);
@ -320,6 +322,9 @@ export interface DeviceProvisionConfiguration {
type: DeviceProvisionType;
provisionDeviceSecret?: string;
provisionDeviceKey?: string;
certificateValue?: string;
certificateRegExPattern?: string;
allowCreateNewDevicesByX509Certificate?: boolean;
}
export function createDeviceProfileConfiguration(type: DeviceProfileType): DeviceProfileConfiguration {

View File

@ -0,0 +1,18 @@
##### X509 Certificate Chain info
X.509 certificates strategy is used to provision devices by client certificates in two-way TLS communication.
<b>This strategy can:</b>
* check for pre-provisioned devices
* update X.509 device credentials
* create new devices
<b>The user uploads</b> X.509 certificate to the device profile and sets a regular expression to fetch the device name from *Common Name (CN)*.
<b>Client certificates must</b> be signed by X.509 certificate, pre-uploaded for this device profile to provision devices by the strategy.
<b>The client must</b> establish a TLS connection using the entire chain of certificates (this chain must include device profile X.509 certificate on the last level).
If a device already exists with outdated X.509 credentials, this strategy automatically updates it with the device certificate's credentials from the chain.
<b>Important:</b> Uploaded certificates should be neither root nor intermediate certificates that are provided by a well-known *Certificate Authority (CA)*.

View File

@ -0,0 +1,15 @@
#### Examples of RegEx usage
* **Pattern:** <code>.*</code> - matches any character (until line terminators)
<br>**CN sample:** <code>DeviceName\nAdditionalInfo</code>
<br>**Pattern matches:** <code>DeviceName</code>
* **Pattern:** <code>^([^@]+)</code> - matches any string that starts with one or more characters that are not the <code>@</code> symbol (<code>@</code> could be replaced by any other symbol)
<br>**CN sample:** <code>DeviceName@AdditionalInfo</code>
<br>**Pattern matches:** <code>DeviceName</code>
* **Pattern:** <code>[\w]*$</code> (equivalent to <code>[a-zA-Z0-9_]\*$</code>) - matches zero or more occurences of any word character (letter, digit or underscore) at the end of the string
<br>**CN sample:** <code>AdditionalInfo2110#DeviceName_01</code>
<br>**Pattern matches:** <code>DeviceName_01</code>
**Note:** Client will get error response in case regex is failed to match.

View File

@ -63,6 +63,8 @@
"print": "Print",
"restore": "Restore",
"confirm": "Confirm",
"more": "More",
"less": "Less",
"skip": "Skip",
"send": "Send"
},
@ -1516,6 +1518,17 @@
"provision-device-secret-required": "Provision device secret is required.",
"copy-provision-secret": "Copy provision secret",
"provision-secret-copied-message": "Provision secret has been copied to clipboard",
"provision-strategy-x509": {
"certificate-chain": "X509 Certificates Chain",
"certificate-chain-hint": "X.509 certificates strategy is used to provision devices by client certificates in two-way TLS communication.",
"allow-create-new-devices": "Create new devices",
"allow-create-new-devices-hint": "If selected new devices will be created and client certificate will be used as device credentials.",
"certificate-value": "Certificate in PEM format",
"certificate-value-required": "Certificate in PEM format is required",
"cn-regex-variable": "CN Regular Expression variable",
"cn-regex-variable-required": "CN Regular Expression variable is required",
"cn-regex-variable-hint": "Required to fetch device name from device's X509 certificate's common name."
},
"condition": "Condition",
"condition-type": "Condition type",
"condition-type-simple": "Simple",