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.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile; 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.StringUtils;
import org.thingsboard.server.common.data.audit.ActionType; 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.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId; 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.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; 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.attributes.AttributesService;
import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceDao; import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceProfileDao;
import org.thingsboard.server.dao.device.DeviceProvisionService; import org.thingsboard.server.dao.device.DeviceProvisionService;
import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.device.provision.ProvisionFailedException; 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.discovery.PartitionService;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.state.DeviceStateService;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Service @Service
@ -77,39 +80,28 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
private static final String DEVICE_PROVISION_STATE = "provisionState"; private static final String DEVICE_PROVISION_STATE = "provisionState";
private static final String PROVISIONED_STATE = "provisioned"; private static final String PROVISIONED_STATE = "provisioned";
@Autowired private final TbClusterService clusterService;
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 public DeviceProvisionServiceImpl(TbQueueProducerProvider producerProvider, TbClusterService clusterService, DeviceProfileService deviceProfileService, DeviceService deviceService, DeviceCredentialsService deviceCredentialsService, AttributesService attributesService, AuditLogService auditLogService, PartitionService partitionService) {
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) {
ruleEngineMsgProducer = producerProvider.getRuleEngineMsgProducer(); 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 @Override
public ProvisionResponse provisionDevice(ProvisionRequest provisionRequest) { public ProvisionResponse provisionDevice(ProvisionRequest provisionRequest) {
fetchAndApplyDeviceNameForX509ProvisionRequestWithRegEx(provisionRequest);
String provisionRequestKey = provisionRequest.getCredentials().getProvisionDeviceKey(); String provisionRequestKey = provisionRequest.getCredentials().getProvisionDeviceKey();
String provisionRequestSecret = provisionRequest.getCredentials().getProvisionDeviceSecret(); String provisionRequestSecret = provisionRequest.getCredentials().getProvisionDeviceSecret();
if (!StringUtils.isEmpty(provisionRequest.getDeviceName())) { if (!StringUtils.isEmpty(provisionRequest.getDeviceName())) {
@ -124,14 +116,14 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name()); throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name());
} }
DeviceProfile targetProfile = deviceProfileDao.findByProvisionDeviceKey(provisionRequestKey); DeviceProfile targetProfile = deviceProfileService.findDeviceProfileByProvisionDeviceKey(provisionRequestKey);
if (targetProfile == null || targetProfile.getProfileData().getProvisionConfiguration() == null || if (targetProfile == null || targetProfile.getProfileData().getProvisionConfiguration() == null ||
targetProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret() == null) { targetProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret() == null) {
throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name()); 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()) { switch (targetProfile.getProvisionType()) {
case ALLOW_CREATE_NEW_DEVICES: case ALLOW_CREATE_NEW_DEVICES:
@ -155,6 +147,25 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
} }
} }
break; 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()); 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) { private ListenableFuture<List<String>> saveProvisionStateAttribute(Device device) {
return attributesService.save(device.getTenantId(), device.getId(), DataConstants.SERVER_SCOPE, return attributesService.save(device.getTenantId(), device.getId(), DataConstants.SERVER_SCOPE,
Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry(DEVICE_PROVISION_STATE, PROVISIONED_STATE), 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; 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); 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 ..."); log.info("Clearing cache to upgrade from version 3.4.2 to 3.4.3 ...");
clearCacheByName("repositorySettings"); clearCacheByName("repositorySettings");
break; break;
case "3.4.4":
log.info("Clearing cache to upgrade from version 3.4.4 to 3.5.0");
clearCacheByName("deviceProfiles");
default: default:
//Do nothing, since cache cleanup is optional. //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.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.device.DeviceCredentialsService; 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.DeviceProvisionService;
import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.device.provision.ProvisionFailedException; import org.thingsboard.server.dao.device.provision.ProvisionFailedException;
import org.thingsboard.server.dao.device.provision.ProvisionRequest; import org.thingsboard.server.dao.device.provision.ProvisionRequest;
import org.thingsboard.server.dao.device.provision.ProvisionResponse; 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.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.relation.RelationService; 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.ConcurrentMap;
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Pattern;
import java.util.stream.Collectors; 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.PASSWORD_MISMATCH;
import static org.thingsboard.server.service.transport.BasicCredentialsValidationResult.VALID; 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 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 TbDeviceProfileCache deviceProfileCache;
private final TbTenantProfileCache tenantProfileCache; private final TbTenantProfileCache tenantProfileCache;
private final TbApiUsageStateService apiUsageStateService; private final TbApiUsageStateService apiUsageStateService;
private final DeviceService deviceService; private final DeviceService deviceService;
private final DeviceProfileService deviceProfileService;
private final RelationService relationService; private final RelationService relationService;
private final DeviceCredentialsService deviceCredentialsService; private final DeviceCredentialsService deviceCredentialsService;
private final DbCallbackExecutorService dbCallbackExecutorService; private final DbCallbackExecutorService dbCallbackExecutorService;
@ -159,6 +166,9 @@ public class DefaultTransportApiService implements TransportApiService {
} else if (transportApiRequestMsg.hasValidateX509CertRequestMsg()) { } else if (transportApiRequestMsg.hasValidateX509CertRequestMsg()) {
ValidateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getValidateX509CertRequestMsg(); ValidateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getValidateX509CertRequestMsg();
result = validateCredentials(msg.getHash(), DeviceCredentialsType.X509_CERTIFICATE); result = validateCredentials(msg.getHash(), DeviceCredentialsType.X509_CERTIFICATE);
} else if (transportApiRequestMsg.hasValidateOrCreateX509CertRequestMsg()) {
TransportProtos.ValidateOrCreateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getValidateOrCreateX509CertRequestMsg();
result = validateOrCreateDeviceX509Certificate(msg.getCertificateChain());
} else if (transportApiRequestMsg.hasGetOrCreateDeviceRequestMsg()) { } else if (transportApiRequestMsg.hasGetOrCreateDeviceRequestMsg()) {
result = handle(transportApiRequestMsg.getGetOrCreateDeviceRequestMsg()); result = handle(transportApiRequestMsg.getGetOrCreateDeviceRequestMsg());
} else if (transportApiRequestMsg.hasEntityProfileRequestMsg()) { } 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) { private ListenableFuture<TransportApiResponseMsg> validateUserNameCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg mqtt) {
DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(mqtt.getUserName()); DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(mqtt.getUserName());
if (credentials != null) { if (credentials != null) {
@ -665,4 +701,24 @@ public class DefaultTransportApiService implements TransportApiService {
private Long checkLong(Long l) { private Long checkLong(Long l) {
return l != null ? l : 0; 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/}" loginProcessingUrl: "${SECURITY_OAUTH2_LOGIN_PROCESSING_URL:/login/oauth2/code/}"
githubMapper: githubMapper:
emailUrl: "${SECURITY_OAUTH2_GITHUB_MAPPER_EMAIL_URL_KEY:https://api.github.com/user/emails}" 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 statistics parameters
usage: usage:

View File

@ -299,6 +299,28 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController
tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, new DataValidationException(msgError)); 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 @Test
public void testChangeDeviceProfileTypeNull() throws Exception { public void testChangeDeviceProfileTypeNull() throws Exception {
DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); 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; string hash = 1;
} }
message ValidateOrCreateDeviceX509CertRequestMsg {
string certificateChain = 1;
}
message ValidateBasicMqttCredRequestMsg { message ValidateBasicMqttCredRequestMsg {
string clientId = 1; string clientId = 1;
string userName = 2; string userName = 2;
@ -942,6 +946,7 @@ message TransportApiRequestMsg {
GetDeviceRequestMsg deviceRequestMsg = 12; GetDeviceRequestMsg deviceRequestMsg = 12;
GetDeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 13; GetDeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 13;
GetAllQueueRoutingInfoRequestMsg getAllQueueRoutingInfoRequestMsg = 14; GetAllQueueRoutingInfoRequestMsg getAllQueueRoutingInfoRequestMsg = 14;
ValidateOrCreateDeviceX509CertRequestMsg validateOrCreateX509CertRequestMsg = 15;
} }
/* Response from ThingsBoard Core Service to Transport Service */ /* Response from ThingsBoard Core Service to Transport Service */

View File

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

View File

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

View File

@ -18,5 +18,6 @@ package org.thingsboard.server.common.data;
public enum DeviceProfileProvisionType { public enum DeviceProfileProvisionType {
DISABLED, DISABLED,
ALLOW_CREATE_NEW_DEVICES, 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({
@JsonSubTypes.Type(value = DisabledDeviceProfileProvisionConfiguration.class, name = "DISABLED"), @JsonSubTypes.Type(value = DisabledDeviceProfileProvisionConfiguration.class, name = "DISABLED"),
@JsonSubTypes.Type(value = AllowCreateNewDevicesDeviceProfileProvisionConfiguration.class, name = "ALLOW_CREATE_NEW_DEVICES"), @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 { public interface DeviceProfileProvisionConfiguration extends Serializable {
String getProvisionDeviceSecret(); 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-----", ""); .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) { public static String pubkTrimNewLines(String input) {
return input.replaceAll("-----BEGIN PUBLIC KEY-----", "") return input.replaceAll("-----BEGIN PUBLIC KEY-----", "")
.replaceAll("\n", "") .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.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.DeviceTransportType; 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.TransportService;
import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.common.transport.TransportServiceCallback;
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; 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.TrustManager;
import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager; import javax.net.ssl.X509TrustManager;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
@ -123,7 +121,7 @@ public class MqttSslHandlerProvider {
static class ThingsboardMqttX509TrustManager implements X509TrustManager { static class ThingsboardMqttX509TrustManager implements X509TrustManager {
private final X509TrustManager trustManager; private final X509TrustManager trustManager;
private TransportService transportService; private final TransportService transportService;
ThingsboardMqttX509TrustManager(X509TrustManager trustManager, TransportService transportService) { ThingsboardMqttX509TrustManager(X509TrustManager trustManager, TransportService transportService) {
this.trustManager = trustManager; this.trustManager = trustManager;
@ -142,17 +140,15 @@ public class MqttSslHandlerProvider {
} }
@Override @Override
public void checkClientTrusted(X509Certificate[] chain, public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
String authType) throws CertificateException { String clientDeviceCertValue = SslUtil.getCertificateString(chain[0]);
String credentialsBody = null;
for (X509Certificate cert : chain) {
try {
String strCert = SslUtil.getCertificateString(cert);
String sha3Hash = EncryptionUtil.getSha3Hash(strCert);
final String[] credentialsBodyHolder = new String[1]; final String[] credentialsBodyHolder = new String[1];
CountDownLatch latch = new CountDownLatch(1); CountDownLatch latch = new CountDownLatch(1);
transportService.process(DeviceTransportType.MQTT, TransportProtos.ValidateDeviceX509CertRequestMsg.newBuilder().setHash(sha3Hash).build(), try {
new TransportServiceCallback<ValidateDeviceCredentialsResponse>() { String certificateChain = SslUtil.getCertificateChainString(chain);
transportService.process(DeviceTransportType.MQTT, TransportProtos.ValidateOrCreateDeviceX509CertRequestMsg
.newBuilder().setCertificateChain(certificateChain).build(),
new TransportServiceCallback<>() {
@Override @Override
public void onSuccess(ValidateDeviceCredentialsResponse msg) { public void onSuccess(ValidateDeviceCredentialsResponse msg) {
if (!StringUtils.isEmpty(msg.getCredentials())) { if (!StringUtils.isEmpty(msg.getCredentials())) {
@ -163,23 +159,22 @@ public class MqttSslHandlerProvider {
@Override @Override
public void onError(Throwable e) { public void onError(Throwable e) {
log.error(e.getMessage(), e); log.trace("Failed to process certificate chain: {}", certificateChain, e);
latch.countDown(); latch.countDown();
} }
}); });
latch.await(10, TimeUnit.SECONDS); latch.await(10, TimeUnit.SECONDS);
if (strCert.equals(credentialsBodyHolder[0])) { if (!clientDeviceCertValue.equals(credentialsBodyHolder[0])) {
credentialsBody = credentialsBodyHolder[0]; log.debug("Failed to find credentials for device certificate chain: {}", chain);
break; 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); 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.TransportToDeviceActorMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ValidateBasicMqttCredRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateBasicMqttCredRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceLwM2MCredentialsRequestMsg; 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.ValidateDeviceX509CertRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ValidateOrCreateDeviceX509CertRequestMsg;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
@ -87,6 +88,9 @@ public interface TransportService {
void process(DeviceTransportType transportType, ValidateDeviceX509CertRequestMsg msg, void process(DeviceTransportType transportType, ValidateDeviceX509CertRequestMsg msg,
TransportServiceCallback<ValidateDeviceCredentialsResponse> callback); TransportServiceCallback<ValidateDeviceCredentialsResponse> callback);
void process(DeviceTransportType transportType, ValidateOrCreateDeviceX509CertRequestMsg msg,
TransportServiceCallback<ValidateDeviceCredentialsResponse> callback);
void process(ValidateDeviceLwM2MCredentialsRequestMsg msg, void process(ValidateDeviceLwM2MCredentialsRequestMsg msg,
TransportServiceCallback<ValidateDeviceCredentialsResponse> callback); 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.TransportDeviceInfo;
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
import org.thingsboard.server.common.transport.limits.TransportRateLimitService; 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.common.transport.util.JsonUtils;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg; 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.provider.TbTransportQueueFactory;
import org.thingsboard.server.queue.scheduler.SchedulerComponent; import org.thingsboard.server.queue.scheduler.SchedulerComponent;
import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.queue.util.TbTransportComponent; import org.thingsboard.server.queue.util.TbTransportComponent;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
@ -434,6 +434,13 @@ public class DefaultTransportService implements TransportService {
doProcess(transportType, protoMsg, callback); 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, private void doProcess(DeviceTransportType transportType, TbProtoQueueMsg<TransportApiRequestMsg> protoMsg,
TransportServiceCallback<ValidateDeviceCredentialsResponse> callback) { TransportServiceCallback<ValidateDeviceCredentialsResponse> callback) {
ListenableFuture<ValidateDeviceCredentialsResponse> response = Futures.transform(transportApiRequestTemplate.send(protoMsg), tmp -> { ListenableFuture<ValidateDeviceCredentialsResponse> response = Futures.transform(transportApiRequestTemplate.send(protoMsg), tmp -> {

View File

@ -16,11 +16,21 @@
package org.thingsboard.server.common.transport.util; package org.thingsboard.server.common.transport.util;
import lombok.extern.slf4j.Slf4j; 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.springframework.util.Base64Utils;
import org.thingsboard.server.common.msg.EncryptionUtil; import org.thingsboard.server.common.msg.EncryptionUtil;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.security.cert.Certificate; import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
/** /**
* @author Valerii Sosliuk * @author Valerii Sosliuk
@ -35,4 +45,44 @@ public class SslUtil {
throws CertificateEncodingException { throws CertificateEncodingException {
return EncryptionUtil.certTrimNewLines(Base64Utils.encodeToString(cert.getEncoded())); 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; package org.thingsboard.server.dao.device;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.SecurityMode; import org.eclipse.leshan.core.SecurityMode;
import org.eclipse.leshan.core.util.SecurityUtil; import org.eclipse.leshan.core.util.SecurityUtil;
import org.hibernate.exception.ConstraintViolationException; import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; 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.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;

View File

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

View File

@ -27,5 +27,6 @@ public class DeviceProfileEvictEvent {
private final String oldName; private final String oldName;
private final DeviceProfileId deviceProfileId; private final DeviceProfileId deviceProfileId;
private final boolean defaultProfile; 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.DefaultDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration; 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.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; 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.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.queue.QueueService; 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.PaginatedRemover;
import org.thingsboard.server.dao.service.Validator; 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.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; 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.validateId;
import static org.thingsboard.server.dao.service.Validator.validateString;
@Service("DeviceProfileDaoService") @Service("DeviceProfileDaoService")
@Slf4j @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_TENANT_ID = "Incorrect tenantId ";
private static final String INCORRECT_DEVICE_PROFILE_ID = "Incorrect deviceProfileId "; 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_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!"; private static final String DEVICE_PROFILE_WITH_SUCH_NAME_ALREADY_EXISTS = "Device profile with such name already exists!";
@Autowired @Autowired
@ -93,13 +103,16 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
if (StringUtils.isNotEmpty(event.getOldName()) && !event.getOldName().equals(event.getNewName())) { if (StringUtils.isNotEmpty(event.getOldName()) && !event.getOldName().equals(event.getNewName())) {
keys.add(DeviceProfileCacheKey.fromName(event.getTenantId(), event.getOldName())); keys.add(DeviceProfileCacheKey.fromName(event.getTenantId(), event.getOldName()));
} }
if (StringUtils.isNotEmpty(event.getProvisionDeviceKey())) {
keys.add(DeviceProfileCacheKey.fromProvisionDeviceKey(event.getProvisionDeviceKey()));
}
cache.evict(keys); cache.evict(keys);
} }
@Override @Override
public DeviceProfile findDeviceProfileById(TenantId tenantId, DeviceProfileId deviceProfileId) { public DeviceProfile findDeviceProfileById(TenantId tenantId, DeviceProfileId deviceProfileId) {
log.trace("Executing findDeviceProfileById [{}]", 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), return cache.getAndPutInTransaction(DeviceProfileCacheKey.fromId(deviceProfileId),
() -> deviceProfileDao.findById(tenantId, deviceProfileId.getId()), true); () -> deviceProfileDao.findById(tenantId, deviceProfileId.getId()), true);
} }
@ -107,30 +120,46 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
@Override @Override
public DeviceProfile findDeviceProfileByName(TenantId tenantId, String profileName) { public DeviceProfile findDeviceProfileByName(TenantId tenantId, String profileName) {
log.trace("Executing findDeviceProfileByName [{}][{}]", tenantId, 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), return cache.getAndPutInTransaction(DeviceProfileCacheKey.fromName(tenantId, profileName),
() -> deviceProfileDao.findByName(tenantId, profileName), true); () -> 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 @Override
public DeviceProfileInfo findDeviceProfileInfoById(TenantId tenantId, DeviceProfileId deviceProfileId) { public DeviceProfileInfo findDeviceProfileInfoById(TenantId tenantId, DeviceProfileId deviceProfileId) {
log.trace("Executing findDeviceProfileById [{}]", 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)); return toDeviceProfileInfo(findDeviceProfileById(tenantId, deviceProfileId));
} }
@Override @Override
public DeviceProfile saveDeviceProfile(DeviceProfile deviceProfile) { public DeviceProfile saveDeviceProfile(DeviceProfile deviceProfile) {
log.trace("Executing saveDeviceProfile [{}]", 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 oldDeviceProfile = deviceProfileValidator.validate(deviceProfile, DeviceProfile::getTenantId);
DeviceProfile savedDeviceProfile; DeviceProfile savedDeviceProfile;
try { try {
savedDeviceProfile = deviceProfileDao.saveAndFlush(deviceProfile.getTenantId(), deviceProfile); savedDeviceProfile = deviceProfileDao.saveAndFlush(deviceProfile.getTenantId(), deviceProfile);
publishEvictEvent(new DeviceProfileEvictEvent(savedDeviceProfile.getTenantId(), savedDeviceProfile.getName(), 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) { } catch (Exception t) {
handleEvictEvent(new DeviceProfileEvictEvent(deviceProfile.getTenantId(), deviceProfile.getName(), 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, checkConstraintViolation(t,
Map.of("device_profile_name_unq_key", DEVICE_PROFILE_WITH_SUCH_NAME_ALREADY_EXISTS, 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!", "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 @Transactional
public void deleteDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId) { public void deleteDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId) {
log.trace("Executing deleteDeviceProfile [{}]", 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()); DeviceProfile deviceProfile = deviceProfileDao.findById(tenantId, deviceProfileId.getId());
if (deviceProfile != null && deviceProfile.isDefault()) { if (deviceProfile != null && deviceProfile.isDefault()) {
throw new DataValidationException("Deletion of Default Device Profile is prohibited!"); throw new DataValidationException("Deletion of Default Device Profile is prohibited!");
@ -170,7 +199,8 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
deleteEntityRelations(tenantId, deviceProfileId); deleteEntityRelations(tenantId, deviceProfileId);
deviceProfileDao.removeById(tenantId, deviceProfileId.getId()); deviceProfileDao.removeById(tenantId, deviceProfileId.getId());
publishEvictEvent(new DeviceProfileEvictEvent(deviceProfile.getTenantId(), deviceProfile.getName(), publishEvictEvent(new DeviceProfileEvictEvent(deviceProfile.getTenantId(), deviceProfile.getName(),
null, deviceProfile.getId(), deviceProfile.isDefault())); null, deviceProfile.getId(), deviceProfile.isDefault(),
deviceProfile.getProvisionDeviceKey()));
} catch (Exception t) { } catch (Exception t) {
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_device_profile")) { if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_device_profile")) {
@ -260,7 +290,7 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
@Override @Override
public boolean setDefaultDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId) { public boolean setDefaultDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId) {
log.trace("Executing setDefaultDeviceProfile [{}]", 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()); DeviceProfile deviceProfile = deviceProfileDao.findById(tenantId, deviceProfileId.getId());
if (!deviceProfile.isDefault()) { if (!deviceProfile.isDefault()) {
deviceProfile.setDefault(true); deviceProfile.setDefault(true);
@ -268,14 +298,14 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
boolean changed = false; boolean changed = false;
if (previousDefaultDeviceProfile == null) { if (previousDefaultDeviceProfile == null) {
deviceProfileDao.save(tenantId, deviceProfile); 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; changed = true;
} else if (!previousDefaultDeviceProfile.getId().equals(deviceProfile.getId())) { } else if (!previousDefaultDeviceProfile.getId().equals(deviceProfile.getId())) {
previousDefaultDeviceProfile.setDefault(false); previousDefaultDeviceProfile.setDefault(false);
deviceProfileDao.save(tenantId, previousDefaultDeviceProfile); deviceProfileDao.save(tenantId, previousDefaultDeviceProfile);
deviceProfileDao.save(tenantId, deviceProfile); deviceProfileDao.save(tenantId, deviceProfile);
publishEvictEvent(new DeviceProfileEvictEvent(previousDefaultDeviceProfile.getTenantId(), previousDefaultDeviceProfile.getName(), null, previousDefaultDeviceProfile.getId(), false)); publishEvictEvent(new DeviceProfileEvictEvent(previousDefaultDeviceProfile.getTenantId(), previousDefaultDeviceProfile.getName(), null, previousDefaultDeviceProfile.getId(), false, deviceProfile.getProvisionDeviceKey()));
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; changed = true;
} }
return changed; return changed;
@ -319,4 +349,38 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
profile.getDefaultDashboardId(), profile.getType(), profile.getTransportType()); 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 com.google.protobuf.DynamicMessage;
import org.eclipse.leshan.core.util.SecurityUtil; import org.eclipse.leshan.core.util.SecurityUtil;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.Base64Utils;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.DeviceProfile; 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.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantService; 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.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -85,6 +98,12 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator<D
@Autowired @Autowired
private DashboardService dashboardService; private DashboardService dashboardService;
@Value("${security.java_cacerts.path}")
private String javaCacertsPath;
@Value("${security.java_cacerts.password}")
private String javaCacertsPassword;
@Override @Override
protected void validateDataImpl(TenantId tenantId, DeviceProfile deviceProfile) { protected void validateDataImpl(TenantId tenantId, DeviceProfile deviceProfile) {
if (StringUtils.isEmpty(deviceProfile.getName())) { if (StringUtils.isEmpty(deviceProfile.getName())) {
@ -118,6 +137,11 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator<D
if (deviceProfile.getProvisionType() == null) { if (deviceProfile.getProvisionType() == null) {
deviceProfile.setProvisionType(DeviceProfileProvisionType.DISABLED); 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(); DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration();
transportConfiguration.validate(); transportConfiguration.validate();
if (transportConfiguration instanceof MqttDeviceProfileTransportConfiguration) { if (transportConfiguration instanceof MqttDeviceProfileTransportConfiguration) {
@ -211,6 +235,11 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator<D
throw new DataValidationException(message); 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; return old;
} }
@ -363,4 +392,27 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator<D
break; 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.user_login_case_sensitive=true
security.claim.allowClaimingByDefault=true security.claim.allowClaimingByDefault=true
security.claim.duration=60000 security.claim.duration=60000
security.java_cacerts.path=/path/to/cacerts/file
security.java_cacerts.password=myPassword
database.ts_max_intervals=700 database.ts_max_intervals=700

View File

@ -27,7 +27,49 @@
{{ 'device-profile.provision-strategy-required' | translate }} {{ 'device-profile.provision-strategy-required' | translate }}
</mat-error> </mat-error>
</mat-form-field> </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-form-field fxFlex class="mat-block">
<mat-label translate>device-profile.provision-device-key</mat-label> <mat-label translate>device-profile.provision-device-key</mat-label>
<input matInput formControlName="provisionDeviceKey" required/> <input matInput formControlName="provisionDeviceKey" required/>
@ -59,4 +101,6 @@
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
</section> </section>
</ng-template>
</div>
</div> </div>

View File

@ -32,7 +32,7 @@ import {
DeviceProvisionType, DeviceProvisionType,
deviceProvisionTypeTranslationMap deviceProvisionTypeTranslationMap
} from '@shared/models/device.models'; } 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 { ActionNotificationShow } from '@core/notification/notification.actions';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -86,14 +86,36 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu
this.provisionConfigurationFormGroup = this.fb.group({ this.provisionConfigurationFormGroup = this.fb.group({
type: [DeviceProvisionType.DISABLED, Validators.required], type: [DeviceProvisionType.DISABLED, Validators.required],
provisionDeviceSecret: [{value: null, disabled: true}, 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) => { this.provisionConfigurationFormGroup.get('type').valueChanges.subscribe((type) => {
if (type === DeviceProvisionType.DISABLED) { if (type === DeviceProvisionType.DISABLED) {
this.provisionConfigurationFormGroup.get('provisionDeviceSecret').disable({emitEvent: false}); for (const field in this.provisionConfigurationFormGroup.controls) {
this.provisionConfigurationFormGroup.get('provisionDeviceSecret').patchValue(null, {emitEvent: false}); if (field !== 'type') {
this.provisionConfigurationFormGroup.get('provisionDeviceKey').disable({emitEvent: false}); const control = this.provisionConfigurationFormGroup.get(field);
this.provisionConfigurationFormGroup.get('provisionDeviceKey').patchValue(null); 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 { } else {
const provisionDeviceSecret: string = this.provisionConfigurationFormGroup.get('provisionDeviceSecret').value; const provisionDeviceSecret: string = this.provisionConfigurationFormGroup.get('provisionDeviceSecret').value;
if (!provisionDeviceSecret || !provisionDeviceSecret.length) { if (!provisionDeviceSecret || !provisionDeviceSecret.length) {
@ -121,6 +143,9 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu
writeValue(value: DeviceProvisionConfiguration | null): void { writeValue(value: DeviceProvisionConfiguration | null): void {
if (isDefinedAndNotNull(value)) { if (isDefinedAndNotNull(value)) {
if (value.type === DeviceProvisionType.X509_CERTIFICATE_CHAIN) {
value.certificateValue = value.provisionDeviceSecret;
}
this.provisionConfigurationFormGroup.patchValue(value, {emitEvent: false}); this.provisionConfigurationFormGroup.patchValue(value, {emitEvent: false});
} else { } else {
this.provisionConfigurationFormGroup.patchValue({type: DeviceProvisionType.DISABLED}); this.provisionConfigurationFormGroup.patchValue({type: DeviceProvisionType.DISABLED});
@ -150,8 +175,12 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu
private updateModel(): void { private updateModel(): void {
let deviceProvisionConfiguration: DeviceProvisionConfiguration = null; let deviceProvisionConfiguration: DeviceProvisionConfiguration = null;
this.resetFormControls(this.provisionConfigurationFormGroup.value);
if (this.provisionConfigurationFormGroup.valid) { if (this.provisionConfigurationFormGroup.valid) {
deviceProvisionConfiguration = this.provisionConfigurationFormGroup.getRawValue(); deviceProvisionConfiguration = this.provisionConfigurationFormGroup.getRawValue();
if (deviceProvisionConfiguration.type === DeviceProvisionType.X509_CERTIFICATE_CHAIN) {
deviceProvisionConfiguration.provisionDeviceSecret = deviceProvisionConfiguration.certificateValue;
}
} }
this.propagateChange(deviceProvisionConfiguration); this.propagateChange(deviceProvisionConfiguration);
} }
@ -166,4 +195,15 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu
horizontalPosition: 'right' 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 = { const deviceProvisionConfiguration: DeviceProvisionConfiguration = {
type: entity?.provisionType ? entity.provisionType : DeviceProvisionType.DISABLED, type: entity?.provisionType ? entity.provisionType : DeviceProvisionType.DISABLED,
provisionDeviceKey: entity?.provisionDeviceKey, 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( const form = this.fb.group(
{ {
@ -185,7 +187,9 @@ export class DeviceProfileComponent extends EntityComponent<DeviceProfile> {
const deviceProvisionConfiguration: DeviceProvisionConfiguration = { const deviceProvisionConfiguration: DeviceProvisionConfiguration = {
type: entity?.provisionType ? entity.provisionType : DeviceProvisionType.DISABLED, type: entity?.provisionType ? entity.provisionType : DeviceProvisionType.DISABLED,
provisionDeviceKey: entity?.provisionDeviceKey, 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({name: entity.name});
this.entityForm.patchValue({type: entity.type}, {emitEvent: false}); this.entityForm.patchValue({type: entity.type}, {emitEvent: false});

View File

@ -169,6 +169,6 @@
<mat-checkbox formControlName="sendAckOnValidationException"> <mat-checkbox formControlName="sendAckOnValidationException">
{{ 'device-profile.mqtt-send-ack-on-validation-exception' | translate }} {{ 'device-profile.mqtt-send-ack-on-validation-exception' | translate }}
</mat-checkbox> </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> </div>
</form> </form>

View File

@ -62,7 +62,8 @@ export enum CoapTransportDeviceType {
export enum DeviceProvisionType { export enum DeviceProvisionType {
DISABLED = 'DISABLED', DISABLED = 'DISABLED',
ALLOW_CREATE_NEW_DEVICES = 'ALLOW_CREATE_NEW_DEVICES', 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 { export interface DeviceConfigurationFormInfo {
@ -110,7 +111,8 @@ export const deviceProvisionTypeTranslationMap = new Map<DeviceProvisionType, st
[ [
[DeviceProvisionType.DISABLED, 'device-profile.provision-strategy-disabled'], [DeviceProvisionType.DISABLED, 'device-profile.provision-strategy-disabled'],
[DeviceProvisionType.ALLOW_CREATE_NEW_DEVICES, 'device-profile.provision-strategy-created-new'], [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; type: DeviceProvisionType;
provisionDeviceSecret?: string; provisionDeviceSecret?: string;
provisionDeviceKey?: string; provisionDeviceKey?: string;
certificateValue?: string;
certificateRegExPattern?: string;
allowCreateNewDevicesByX509Certificate?: boolean;
} }
export function createDeviceProfileConfiguration(type: DeviceProfileType): DeviceProfileConfiguration { 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", "print": "Print",
"restore": "Restore", "restore": "Restore",
"confirm": "Confirm", "confirm": "Confirm",
"more": "More",
"less": "Less",
"skip": "Skip", "skip": "Skip",
"send": "Send" "send": "Send"
}, },
@ -1516,6 +1518,17 @@
"provision-device-secret-required": "Provision device secret is required.", "provision-device-secret-required": "Provision device secret is required.",
"copy-provision-secret": "Copy provision secret", "copy-provision-secret": "Copy provision secret",
"provision-secret-copied-message": "Provision secret has been copied to clipboard", "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": "Condition",
"condition-type": "Condition type", "condition-type": "Condition type",
"condition-type-simple": "Simple", "condition-type-simple": "Simple",