Merge branch 'feature/x509-device-cert-impr' of github.com:AndriiLandiak/thingsboard into feature/x509-device-provisioning
This commit is contained in:
commit
4cea4362dc
@ -20,15 +20,16 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.thingsboard.common.util.JacksonUtil;
|
||||
import org.thingsboard.server.cluster.TbClusterService;
|
||||
import org.thingsboard.server.common.data.DataConstants;
|
||||
import org.thingsboard.server.common.data.Device;
|
||||
import org.thingsboard.server.common.data.DeviceProfile;
|
||||
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
|
||||
import org.thingsboard.server.common.data.StringUtils;
|
||||
import org.thingsboard.server.common.data.audit.ActionType;
|
||||
import org.thingsboard.server.common.data.device.profile.X509CertificateChainProvisionConfiguration;
|
||||
import org.thingsboard.server.common.data.id.CustomerId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.common.data.id.UserId;
|
||||
@ -36,15 +37,16 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry;
|
||||
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
|
||||
import org.thingsboard.server.common.data.kv.StringDataEntry;
|
||||
import org.thingsboard.server.common.data.security.DeviceCredentials;
|
||||
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
|
||||
import org.thingsboard.server.common.msg.TbMsg;
|
||||
import org.thingsboard.server.common.msg.TbMsgMetaData;
|
||||
import org.thingsboard.server.common.msg.queue.ServiceType;
|
||||
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
|
||||
import org.thingsboard.server.common.transport.util.SslUtil;
|
||||
import org.thingsboard.server.dao.attributes.AttributesService;
|
||||
import org.thingsboard.server.dao.audit.AuditLogService;
|
||||
import org.thingsboard.server.dao.device.DeviceCredentialsService;
|
||||
import org.thingsboard.server.dao.device.DeviceDao;
|
||||
import org.thingsboard.server.dao.device.DeviceProfileDao;
|
||||
import org.thingsboard.server.dao.device.DeviceProfileService;
|
||||
import org.thingsboard.server.dao.device.DeviceProvisionService;
|
||||
import org.thingsboard.server.dao.device.DeviceService;
|
||||
import org.thingsboard.server.dao.device.provision.ProvisionFailedException;
|
||||
@ -59,12 +61,13 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg;
|
||||
import org.thingsboard.server.queue.discovery.PartitionService;
|
||||
import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
|
||||
import org.thingsboard.server.queue.util.TbCoreComponent;
|
||||
import org.thingsboard.server.service.state.DeviceStateService;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
||||
@Service
|
||||
@ -77,39 +80,28 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
|
||||
private static final String DEVICE_PROVISION_STATE = "provisionState";
|
||||
private static final String PROVISIONED_STATE = "provisioned";
|
||||
|
||||
@Autowired
|
||||
TbClusterService clusterService;
|
||||
private final TbClusterService clusterService;
|
||||
private final DeviceProfileService deviceProfileService;
|
||||
private final DeviceService deviceService;
|
||||
private final DeviceCredentialsService deviceCredentialsService;
|
||||
private final AttributesService attributesService;
|
||||
private final AuditLogService auditLogService;
|
||||
private final PartitionService partitionService;
|
||||
|
||||
@Autowired
|
||||
DeviceDao deviceDao;
|
||||
|
||||
@Autowired
|
||||
DeviceProfileDao deviceProfileDao;
|
||||
|
||||
@Autowired
|
||||
DeviceService deviceService;
|
||||
|
||||
@Autowired
|
||||
DeviceCredentialsService deviceCredentialsService;
|
||||
|
||||
@Autowired
|
||||
AttributesService attributesService;
|
||||
|
||||
@Autowired
|
||||
DeviceStateService deviceStateService;
|
||||
|
||||
@Autowired
|
||||
AuditLogService auditLogService;
|
||||
|
||||
@Autowired
|
||||
PartitionService partitionService;
|
||||
|
||||
public DeviceProvisionServiceImpl(TbQueueProducerProvider producerProvider) {
|
||||
public DeviceProvisionServiceImpl(TbQueueProducerProvider producerProvider, TbClusterService clusterService, DeviceProfileService deviceProfileService, DeviceService deviceService, DeviceCredentialsService deviceCredentialsService, AttributesService attributesService, AuditLogService auditLogService, PartitionService partitionService) {
|
||||
ruleEngineMsgProducer = producerProvider.getRuleEngineMsgProducer();
|
||||
this.clusterService = clusterService;
|
||||
this.deviceProfileService = deviceProfileService;
|
||||
this.deviceService = deviceService;
|
||||
this.deviceCredentialsService = deviceCredentialsService;
|
||||
this.attributesService = attributesService;
|
||||
this.auditLogService = auditLogService;
|
||||
this.partitionService = partitionService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProvisionResponse provisionDevice(ProvisionRequest provisionRequest) {
|
||||
fetchAndApplyDeviceNameForX509ProvisionRequestWithRegEx(provisionRequest);
|
||||
String provisionRequestKey = provisionRequest.getCredentials().getProvisionDeviceKey();
|
||||
String provisionRequestSecret = provisionRequest.getCredentials().getProvisionDeviceSecret();
|
||||
if (!StringUtils.isEmpty(provisionRequest.getDeviceName())) {
|
||||
@ -124,14 +116,14 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
|
||||
throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name());
|
||||
}
|
||||
|
||||
DeviceProfile targetProfile = deviceProfileDao.findByProvisionDeviceKey(provisionRequestKey);
|
||||
DeviceProfile targetProfile = deviceProfileService.findDeviceProfileByProvisionDeviceKey(provisionRequestKey);
|
||||
|
||||
if (targetProfile == null || targetProfile.getProfileData().getProvisionConfiguration() == null ||
|
||||
targetProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret() == null) {
|
||||
throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name());
|
||||
}
|
||||
|
||||
Device targetDevice = deviceDao.findDeviceByTenantIdAndName(targetProfile.getTenantId().getId(), provisionRequest.getDeviceName()).orElse(null);
|
||||
Device targetDevice = deviceService.findDeviceByTenantIdAndName(targetProfile.getTenantId(), provisionRequest.getDeviceName());
|
||||
|
||||
switch (targetProfile.getProvisionType()) {
|
||||
case ALLOW_CREATE_NEW_DEVICES:
|
||||
@ -155,6 +147,25 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
|
||||
}
|
||||
}
|
||||
break;
|
||||
case X509_CERTIFICATE_CHAIN:
|
||||
if (targetProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret().equals(provisionRequestSecret)) {
|
||||
X509CertificateChainProvisionConfiguration x509Configuration = (X509CertificateChainProvisionConfiguration) targetProfile.getProfileData().getProvisionConfiguration();
|
||||
if (targetDevice != null && targetDevice.getDeviceProfileId().equals(targetProfile.getId())) {
|
||||
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(targetDevice.getTenantId(), targetDevice.getId());
|
||||
if (deviceCredentials.getCredentialsType() == DeviceCredentialsType.X509_CERTIFICATE) {
|
||||
String updatedDeviceCertificateValue = provisionRequest.getCredentialsData().getX509CertHash();
|
||||
deviceCredentials = updateDeviceCredentials(targetDevice.getTenantId(), deviceCredentials,
|
||||
updatedDeviceCertificateValue, DeviceCredentialsType.X509_CERTIFICATE);
|
||||
}
|
||||
return new ProvisionResponse(deviceCredentials, ProvisionResponseStatus.SUCCESS);
|
||||
} else if (x509Configuration.isAllowCreateNewDevicesByX509Certificate()) {
|
||||
return createDevice(provisionRequest, targetProfile);
|
||||
} else {
|
||||
log.warn("Device with name {} doesn't exist and cannot be created due incorrect configuration for X509CertificateChainProvisionConfiguration", provisionRequest.getDeviceName());
|
||||
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name());
|
||||
}
|
||||
@ -209,6 +220,14 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
|
||||
}
|
||||
}
|
||||
|
||||
private DeviceCredentials updateDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials, String certificateValue,
|
||||
DeviceCredentialsType credentialsType) {
|
||||
log.trace("Updating device credentials [{}] with certificate value [{}]", deviceCredentials, certificateValue);
|
||||
deviceCredentials.setCredentialsValue(certificateValue);
|
||||
deviceCredentials.setCredentialsType(credentialsType);
|
||||
return deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials);
|
||||
}
|
||||
|
||||
private ListenableFuture<List<String>> saveProvisionStateAttribute(Device device) {
|
||||
return attributesService.save(device.getTenantId(), device.getId(), DataConstants.SERVER_SCOPE,
|
||||
Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry(DEVICE_PROVISION_STATE, PROVISIONED_STATE),
|
||||
@ -257,4 +276,32 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
|
||||
ActionType actionType = success ? ActionType.PROVISION_SUCCESS : ActionType.PROVISION_FAILURE;
|
||||
auditLogService.logEntityAction(tenantId, customerId, new UserId(UserId.NULL_UUID), device.getName(), device.getId(), device, actionType, null, provisionRequest);
|
||||
}
|
||||
|
||||
private void fetchAndApplyDeviceNameForX509ProvisionRequestWithRegEx(ProvisionRequest provisionRequest) {
|
||||
DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileByProvisionDeviceKey(provisionRequest.getCredentials().getProvisionDeviceKey());
|
||||
if (deviceProfile != null && DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN.equals(deviceProfile.getProfileData().getProvisionConfiguration().getType())) {
|
||||
X509CertificateChainProvisionConfiguration configuration = (X509CertificateChainProvisionConfiguration) deviceProfile.getProfileData().getProvisionConfiguration();
|
||||
String certificateValue = provisionRequest.getCredentialsData().getX509CertHash();
|
||||
String certificateRegEx = configuration.getCertificateRegExPattern();
|
||||
String deviceName = extractDeviceNameFromCertificateCNByRegEx(certificateValue, certificateRegEx);
|
||||
if (deviceName == null) {
|
||||
log.warn("Device name cannot be extracted using regex [{}] for certificate [{}]",certificateRegEx, certificateValue);
|
||||
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
|
||||
}
|
||||
provisionRequest.setDeviceName(deviceName);
|
||||
}
|
||||
}
|
||||
|
||||
private String extractDeviceNameFromCertificateCNByRegEx(String x509Value, String regex) {
|
||||
try {
|
||||
String commonName = SslUtil.parseCommonName(SslUtil.readCertFile(x509Value));
|
||||
log.trace("Extract CN [{}] by regex pattern [{}]", commonName, regex);
|
||||
Pattern pattern = Pattern.compile(regex);
|
||||
Matcher matcher = pattern.matcher(commonName);
|
||||
if (matcher.find()) {
|
||||
return matcher.group(0);
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,6 +82,9 @@ public class DefaultCacheCleanupService implements CacheCleanupService {
|
||||
log.info("Clearing cache to upgrade from version 3.4.2 to 3.4.3 ...");
|
||||
clearCacheByName("repositorySettings");
|
||||
break;
|
||||
case "3.4.4":
|
||||
log.info("Clearing cache to upgrade from version 3.4.4 to 3.5.0");
|
||||
clearCacheByName("deviceProfiles");
|
||||
default:
|
||||
//Do nothing, since cache cleanup is optional.
|
||||
}
|
||||
|
||||
@ -66,11 +66,13 @@ import org.thingsboard.server.common.msg.TbMsg;
|
||||
import org.thingsboard.server.common.msg.TbMsgDataType;
|
||||
import org.thingsboard.server.common.msg.TbMsgMetaData;
|
||||
import org.thingsboard.server.dao.device.DeviceCredentialsService;
|
||||
import org.thingsboard.server.dao.device.DeviceProfileService;
|
||||
import org.thingsboard.server.dao.device.DeviceProvisionService;
|
||||
import org.thingsboard.server.dao.device.DeviceService;
|
||||
import org.thingsboard.server.dao.device.provision.ProvisionFailedException;
|
||||
import org.thingsboard.server.dao.device.provision.ProvisionRequest;
|
||||
import org.thingsboard.server.dao.device.provision.ProvisionResponse;
|
||||
import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus;
|
||||
import org.thingsboard.server.dao.ota.OtaPackageService;
|
||||
import org.thingsboard.server.dao.queue.QueueService;
|
||||
import org.thingsboard.server.dao.relation.RelationService;
|
||||
@ -108,8 +110,10 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.thingsboard.server.common.data.DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN;
|
||||
import static org.thingsboard.server.service.transport.BasicCredentialsValidationResult.PASSWORD_MISMATCH;
|
||||
import static org.thingsboard.server.service.transport.BasicCredentialsValidationResult.VALID;
|
||||
|
||||
@ -124,10 +128,13 @@ public class DefaultTransportApiService implements TransportApiService {
|
||||
|
||||
private static final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
private static final Pattern X509_CERTIFICATE_TRIM_CHAIN_PATTERN = Pattern.compile("-----BEGIN CERTIFICATE-----\\s*.*?\\s*-----END CERTIFICATE-----");
|
||||
|
||||
private final TbDeviceProfileCache deviceProfileCache;
|
||||
private final TbTenantProfileCache tenantProfileCache;
|
||||
private final TbApiUsageStateService apiUsageStateService;
|
||||
private final DeviceService deviceService;
|
||||
private final DeviceProfileService deviceProfileService;
|
||||
private final RelationService relationService;
|
||||
private final DeviceCredentialsService deviceCredentialsService;
|
||||
private final DbCallbackExecutorService dbCallbackExecutorService;
|
||||
@ -159,6 +166,9 @@ public class DefaultTransportApiService implements TransportApiService {
|
||||
} else if (transportApiRequestMsg.hasValidateX509CertRequestMsg()) {
|
||||
ValidateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getValidateX509CertRequestMsg();
|
||||
result = validateCredentials(msg.getHash(), DeviceCredentialsType.X509_CERTIFICATE);
|
||||
} else if (transportApiRequestMsg.hasValidateOrCreateX509CertRequestMsg()) {
|
||||
TransportProtos.ValidateOrCreateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getValidateOrCreateX509CertRequestMsg();
|
||||
result = validateOrCreateDeviceX509Certificate(msg.getCertificateChain());
|
||||
} else if (transportApiRequestMsg.hasGetOrCreateDeviceRequestMsg()) {
|
||||
result = handle(transportApiRequestMsg.getGetOrCreateDeviceRequestMsg());
|
||||
} else if (transportApiRequestMsg.hasEntityProfileRequestMsg()) {
|
||||
@ -226,6 +236,32 @@ public class DefaultTransportApiService implements TransportApiService {
|
||||
}
|
||||
}
|
||||
|
||||
protected ListenableFuture<TransportApiResponseMsg> validateOrCreateDeviceX509Certificate(String certificateChain) {
|
||||
List<String> chain = X509_CERTIFICATE_TRIM_CHAIN_PATTERN.matcher(certificateChain).results().map(match ->
|
||||
EncryptionUtil.certTrimNewLines(match.group())).collect(Collectors.toList());
|
||||
for (String certificateValue : chain) {
|
||||
String certificateHash = EncryptionUtil.getSha3Hash(certificateValue);
|
||||
DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(certificateHash);
|
||||
if (credentials != null && credentials.getCredentialsType() == DeviceCredentialsType.X509_CERTIFICATE) {
|
||||
return getDeviceInfo(credentials);
|
||||
}
|
||||
DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileByProvisionDeviceKey(certificateHash);
|
||||
if (deviceProfile != null && deviceProfile.getProvisionType() == X509_CERTIFICATE_CHAIN) {
|
||||
String updatedDeviceProvisionSecret = chain.get(0);
|
||||
ProvisionRequest provisionRequest = createProvisionRequest(deviceProfile, updatedDeviceProvisionSecret);
|
||||
ProvisionResponse provisionResponse = provisionDeviceRequestAndGetResponse(provisionRequest);
|
||||
if (provisionResponse != null && ProvisionResponseStatus.SUCCESS.equals(provisionResponse.getResponseStatus())) {
|
||||
return getDeviceInfo(provisionResponse.getDeviceCredentials());
|
||||
} else {
|
||||
return getEmptyTransportApiResponseFuture();
|
||||
}
|
||||
} else if (deviceProfile != null) {
|
||||
log.warn("[{}] Device Profile provision configuration mismatched: expected {}, actual {}", deviceProfile.getId(), X509_CERTIFICATE_CHAIN, deviceProfile.getProvisionType());
|
||||
}
|
||||
}
|
||||
return getEmptyTransportApiResponseFuture();
|
||||
}
|
||||
|
||||
private ListenableFuture<TransportApiResponseMsg> validateUserNameCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg mqtt) {
|
||||
DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(mqtt.getUserName());
|
||||
if (credentials != null) {
|
||||
@ -665,4 +701,24 @@ public class DefaultTransportApiService implements TransportApiService {
|
||||
private Long checkLong(Long l) {
|
||||
return l != null ? l : 0;
|
||||
}
|
||||
|
||||
private ProvisionRequest createProvisionRequest(DeviceProfile deviceProfile, String certificateValue) {
|
||||
ProvisionDeviceProfileCredentials provisionDeviceProfileCredentials = new ProvisionDeviceProfileCredentials(
|
||||
deviceProfile.getProvisionDeviceKey(),
|
||||
deviceProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret()
|
||||
);
|
||||
ProvisionDeviceCredentialsData provisionDeviceCredentialsData = new ProvisionDeviceCredentialsData(null, null, null, null, certificateValue);
|
||||
|
||||
return new ProvisionRequest(null, DeviceCredentialsType.X509_CERTIFICATE, provisionDeviceCredentialsData, provisionDeviceProfileCredentials);
|
||||
}
|
||||
|
||||
private ProvisionResponse provisionDeviceRequestAndGetResponse(ProvisionRequest provisionRequest) {
|
||||
try {
|
||||
return deviceProvisionService.provisionDevice(provisionRequest);
|
||||
} catch (ProvisionFailedException e) {
|
||||
log.error(e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -130,6 +130,9 @@ security:
|
||||
loginProcessingUrl: "${SECURITY_OAUTH2_LOGIN_PROCESSING_URL:/login/oauth2/code/}"
|
||||
githubMapper:
|
||||
emailUrl: "${SECURITY_OAUTH2_GITHUB_MAPPER_EMAIL_URL_KEY:https://api.github.com/user/emails}"
|
||||
java_cacerts:
|
||||
path: "${SECURITY_JAVA_CACERTS_PATH:${java.home}${file.separator}lib${file.separator}security${file.separator}cacerts}"
|
||||
password: "${SECURITY_JAVA_CACERTS_PASSWORD:changeit}"
|
||||
|
||||
# Usage statistics parameters
|
||||
usage:
|
||||
|
||||
@ -299,6 +299,28 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController
|
||||
tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, new DataValidationException(msgError));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSaveDeviceProfileWithSameCertificateHash() throws Exception {
|
||||
DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile");
|
||||
deviceProfile.setProvisionDeviceKey("Certificate hash");
|
||||
|
||||
doPost("/api/deviceProfile", deviceProfile)
|
||||
.andExpect(status().isOk());
|
||||
|
||||
DeviceProfile deviceProfile2 = this.createDeviceProfile("Device Profile 2");
|
||||
deviceProfile2.setProvisionDeviceKey("Certificate hash");
|
||||
|
||||
Mockito.reset(tbClusterService, auditLogService);
|
||||
|
||||
String msgError = "Device profile with such provision device key already exists!";
|
||||
doPost("/api/deviceProfile", deviceProfile2)
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(statusReason(containsString(msgError)));
|
||||
|
||||
testNotifyEntityEqualsOneTimeServiceNeverError(deviceProfile, savedTenant.getId(),
|
||||
tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, new DataValidationException(msgError));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChangeDeviceProfileTypeNull() throws Exception {
|
||||
DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile");
|
||||
|
||||
@ -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]);
|
||||
}
|
||||
}
|
||||
@ -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-----
|
||||
@ -179,6 +179,10 @@ message ValidateDeviceX509CertRequestMsg {
|
||||
string hash = 1;
|
||||
}
|
||||
|
||||
message ValidateOrCreateDeviceX509CertRequestMsg {
|
||||
string certificateChain = 1;
|
||||
}
|
||||
|
||||
message ValidateBasicMqttCredRequestMsg {
|
||||
string clientId = 1;
|
||||
string userName = 2;
|
||||
@ -942,6 +946,7 @@ message TransportApiRequestMsg {
|
||||
GetDeviceRequestMsg deviceRequestMsg = 12;
|
||||
GetDeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 13;
|
||||
GetAllQueueRoutingInfoRequestMsg getAllQueueRoutingInfoRequestMsg = 14;
|
||||
ValidateOrCreateDeviceX509CertRequestMsg validateOrCreateX509CertRequestMsg = 15;
|
||||
}
|
||||
|
||||
/* Response from ThingsBoard Core Service to Transport Service */
|
||||
|
||||
@ -15,10 +15,10 @@
|
||||
*/
|
||||
package org.thingsboard.server.dao.device;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import org.thingsboard.server.common.data.id.DeviceId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.common.data.security.DeviceCredentials;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
public interface DeviceCredentialsService {
|
||||
|
||||
|
||||
@ -39,6 +39,8 @@ public interface DeviceProfileService extends EntityDaoService {
|
||||
|
||||
PageData<DeviceProfileInfo> findDeviceProfileInfos(TenantId tenantId, PageLink pageLink, String transportType);
|
||||
|
||||
DeviceProfile findDeviceProfileByProvisionDeviceKey(String provisionDeviceKey);
|
||||
|
||||
DeviceProfile findOrCreateDeviceProfile(TenantId tenantId, String profileName);
|
||||
|
||||
DeviceProfile createDefaultDeviceProfile(TenantId tenantId);
|
||||
|
||||
@ -18,5 +18,6 @@ package org.thingsboard.server.common.data;
|
||||
public enum DeviceProfileProvisionType {
|
||||
DISABLED,
|
||||
ALLOW_CREATE_NEW_DEVICES,
|
||||
CHECK_PRE_PROVISIONED_DEVICES
|
||||
CHECK_PRE_PROVISIONED_DEVICES,
|
||||
X509_CERTIFICATE_CHAIN
|
||||
}
|
||||
|
||||
@ -31,7 +31,8 @@ import java.io.Serializable;
|
||||
@JsonSubTypes({
|
||||
@JsonSubTypes.Type(value = DisabledDeviceProfileProvisionConfiguration.class, name = "DISABLED"),
|
||||
@JsonSubTypes.Type(value = AllowCreateNewDevicesDeviceProfileProvisionConfiguration.class, name = "ALLOW_CREATE_NEW_DEVICES"),
|
||||
@JsonSubTypes.Type(value = CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.class, name = "CHECK_PRE_PROVISIONED_DEVICES")})
|
||||
@JsonSubTypes.Type(value = CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.class, name = "CHECK_PRE_PROVISIONED_DEVICES"),
|
||||
@JsonSubTypes.Type(value = X509CertificateChainProvisionConfiguration.class, name = "X509_CERTIFICATE_CHAIN")})
|
||||
public interface DeviceProfileProvisionConfiguration extends Serializable {
|
||||
|
||||
String getProvisionDeviceSecret();
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -35,6 +35,14 @@ public class EncryptionUtil {
|
||||
.replaceAll("-----END CERTIFICATE-----", "");
|
||||
}
|
||||
|
||||
public static String certTrimNewLinesForChainInDeviceProfile(String input) {
|
||||
return input.replaceAll("\n", "")
|
||||
.replaceAll("\r", "")
|
||||
.replaceAll("-----BEGIN CERTIFICATE-----", "-----BEGIN CERTIFICATE-----\n")
|
||||
.replaceAll("-----END CERTIFICATE-----", "\n-----END CERTIFICATE-----\n")
|
||||
.trim();
|
||||
}
|
||||
|
||||
public static String pubkTrimNewLines(String input) {
|
||||
return input.replaceAll("-----BEGIN PUBLIC KEY-----", "")
|
||||
.replaceAll("\n", "")
|
||||
|
||||
@ -24,9 +24,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.thingsboard.server.common.data.StringUtils;
|
||||
import org.thingsboard.server.common.data.DeviceTransportType;
|
||||
import org.thingsboard.server.common.msg.EncryptionUtil;
|
||||
import org.thingsboard.server.common.data.StringUtils;
|
||||
import org.thingsboard.server.common.transport.TransportService;
|
||||
import org.thingsboard.server.common.transport.TransportServiceCallback;
|
||||
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
|
||||
@ -42,7 +41,6 @@ import javax.net.ssl.SSLEngine;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
@ -123,7 +121,7 @@ public class MqttSslHandlerProvider {
|
||||
static class ThingsboardMqttX509TrustManager implements X509TrustManager {
|
||||
|
||||
private final X509TrustManager trustManager;
|
||||
private TransportService transportService;
|
||||
private final TransportService transportService;
|
||||
|
||||
ThingsboardMqttX509TrustManager(X509TrustManager trustManager, TransportService transportService) {
|
||||
this.trustManager = trustManager;
|
||||
@ -142,43 +140,40 @@ public class MqttSslHandlerProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] chain,
|
||||
String authType) throws CertificateException {
|
||||
String credentialsBody = null;
|
||||
for (X509Certificate cert : chain) {
|
||||
try {
|
||||
String strCert = SslUtil.getCertificateString(cert);
|
||||
String sha3Hash = EncryptionUtil.getSha3Hash(strCert);
|
||||
final String[] credentialsBodyHolder = new String[1];
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
transportService.process(DeviceTransportType.MQTT, TransportProtos.ValidateDeviceX509CertRequestMsg.newBuilder().setHash(sha3Hash).build(),
|
||||
new TransportServiceCallback<ValidateDeviceCredentialsResponse>() {
|
||||
@Override
|
||||
public void onSuccess(ValidateDeviceCredentialsResponse msg) {
|
||||
if (!StringUtils.isEmpty(msg.getCredentials())) {
|
||||
credentialsBodyHolder[0] = msg.getCredentials();
|
||||
}
|
||||
latch.countDown();
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
||||
String clientDeviceCertValue = SslUtil.getCertificateString(chain[0]);
|
||||
final String[] credentialsBodyHolder = new String[1];
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
try {
|
||||
String certificateChain = SslUtil.getCertificateChainString(chain);
|
||||
transportService.process(DeviceTransportType.MQTT, TransportProtos.ValidateOrCreateDeviceX509CertRequestMsg
|
||||
.newBuilder().setCertificateChain(certificateChain).build(),
|
||||
new TransportServiceCallback<>() {
|
||||
@Override
|
||||
public void onSuccess(ValidateDeviceCredentialsResponse msg) {
|
||||
if (!StringUtils.isEmpty(msg.getCredentials())) {
|
||||
credentialsBodyHolder[0] = msg.getCredentials();
|
||||
}
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable e) {
|
||||
log.error(e.getMessage(), e);
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
latch.await(10, TimeUnit.SECONDS);
|
||||
if (strCert.equals(credentialsBodyHolder[0])) {
|
||||
credentialsBody = credentialsBodyHolder[0];
|
||||
break;
|
||||
@Override
|
||||
public void onError(Throwable e) {
|
||||
log.trace("Failed to process certificate chain: {}", certificateChain, e);
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
latch.await(10, TimeUnit.SECONDS);
|
||||
if (!clientDeviceCertValue.equals(credentialsBodyHolder[0])) {
|
||||
log.debug("Failed to find credentials for device certificate chain: {}", chain);
|
||||
if (chain.length == 1) {
|
||||
throw new CertificateException("Invalid Device Certificate");
|
||||
} else {
|
||||
throw new CertificateException("Invalid Chain of X509 Certificates");
|
||||
}
|
||||
} catch (InterruptedException | CertificateEncodingException e) {
|
||||
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");
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,8 +54,9 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcRequestMs
|
||||
import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos.ValidateBasicMqttCredRequestMsg;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceLwM2MCredentialsRequestMsg;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos.ValidateOrCreateDeviceX509CertRequestMsg;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@ -87,6 +88,9 @@ public interface TransportService {
|
||||
void process(DeviceTransportType transportType, ValidateDeviceX509CertRequestMsg msg,
|
||||
TransportServiceCallback<ValidateDeviceCredentialsResponse> callback);
|
||||
|
||||
void process(DeviceTransportType transportType, ValidateOrCreateDeviceX509CertRequestMsg msg,
|
||||
TransportServiceCallback<ValidateDeviceCredentialsResponse> callback);
|
||||
|
||||
void process(ValidateDeviceLwM2MCredentialsRequestMsg msg,
|
||||
TransportServiceCallback<ValidateDeviceCredentialsResponse> callback);
|
||||
|
||||
|
||||
@ -72,7 +72,6 @@ import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGateway
|
||||
import org.thingsboard.server.common.transport.auth.TransportDeviceInfo;
|
||||
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
|
||||
import org.thingsboard.server.common.transport.limits.TransportRateLimitService;
|
||||
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
|
||||
import org.thingsboard.server.common.transport.util.JsonUtils;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg;
|
||||
@ -97,6 +96,7 @@ import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
|
||||
import org.thingsboard.server.queue.provider.TbTransportQueueFactory;
|
||||
import org.thingsboard.server.queue.scheduler.SchedulerComponent;
|
||||
import org.thingsboard.server.queue.util.AfterStartUp;
|
||||
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
|
||||
import org.thingsboard.server.queue.util.TbTransportComponent;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
@ -434,6 +434,13 @@ public class DefaultTransportService implements TransportService {
|
||||
doProcess(transportType, protoMsg, callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(DeviceTransportType transportType, TransportProtos.ValidateOrCreateDeviceX509CertRequestMsg msg, TransportServiceCallback<ValidateDeviceCredentialsResponse> callback) {
|
||||
log.trace("Processing msg: {}", msg);
|
||||
TbProtoQueueMsg<TransportApiRequestMsg> protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setValidateOrCreateX509CertRequestMsg(msg).build());
|
||||
doProcess(transportType, protoMsg, callback);
|
||||
}
|
||||
|
||||
private void doProcess(DeviceTransportType transportType, TbProtoQueueMsg<TransportApiRequestMsg> protoMsg,
|
||||
TransportServiceCallback<ValidateDeviceCredentialsResponse> callback) {
|
||||
ListenableFuture<ValidateDeviceCredentialsResponse> response = Futures.transform(transportApiRequestTemplate.send(protoMsg), tmp -> {
|
||||
|
||||
@ -16,11 +16,21 @@
|
||||
package org.thingsboard.server.common.transport.util;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.bouncycastle.asn1.x500.RDN;
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.asn1.x500.style.BCStyle;
|
||||
import org.bouncycastle.asn1.x500.style.IETFUtils;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
|
||||
import org.springframework.util.Base64Utils;
|
||||
import org.thingsboard.server.common.msg.EncryptionUtil;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
/**
|
||||
* @author Valerii Sosliuk
|
||||
@ -35,4 +45,44 @@ public class SslUtil {
|
||||
throws CertificateEncodingException {
|
||||
return EncryptionUtil.certTrimNewLines(Base64Utils.encodeToString(cert.getEncoded()));
|
||||
}
|
||||
|
||||
public static String getCertificateChainString(Certificate[] chain)
|
||||
throws CertificateEncodingException {
|
||||
String begin = "-----BEGIN CERTIFICATE-----";
|
||||
String end = "-----END CERTIFICATE-----";
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
for (Certificate cert: chain) {
|
||||
stringBuilder.append(begin).append(EncryptionUtil.certTrimNewLines(Base64Utils.encodeToString(cert.getEncoded()))).append(end).append("\n");
|
||||
}
|
||||
return stringBuilder.toString();
|
||||
}
|
||||
|
||||
public static X509Certificate readCertFile(String fileContent) {
|
||||
X509Certificate certificate = null;
|
||||
try {
|
||||
if (fileContent != null && !fileContent.trim().isEmpty()) {
|
||||
fileContent = fileContent.replace("-----BEGIN CERTIFICATE-----", "")
|
||||
.replace("-----END CERTIFICATE-----", "")
|
||||
.replaceAll("\\s", "");
|
||||
byte[] decoded = Base64.decodeBase64(fileContent);
|
||||
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
|
||||
try (InputStream inStream = new ByteArrayInputStream(decoded)) {
|
||||
certificate = (X509Certificate) certFactory.generateCertificate(inStream);
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
return certificate;
|
||||
}
|
||||
|
||||
public static String parseCommonName(X509Certificate certificate) {
|
||||
X500Name x500name;
|
||||
try {
|
||||
x500name = new JcaX509CertificateHolder(certificate).getSubject();
|
||||
} catch (CertificateEncodingException e) {
|
||||
log.warn("Cannot parse CN from device certificate");
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
RDN cn = x500name.getRDNs(BCStyle.CN)[0];
|
||||
return IETFUtils.valueToString(cn.getFirst().getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,15 +16,12 @@
|
||||
package org.thingsboard.server.dao.device;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.leshan.core.SecurityMode;
|
||||
import org.eclipse.leshan.core.util.SecurityUtil;
|
||||
import org.hibernate.exception.ConstraintViolationException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.event.TransactionalEventListener;
|
||||
import org.thingsboard.common.util.JacksonUtil;
|
||||
import org.thingsboard.server.common.data.StringUtils;
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
package org.thingsboard.server.dao.device;
|
||||
|
||||
import lombok.Data;
|
||||
import org.thingsboard.server.common.data.StringUtils;
|
||||
import org.thingsboard.server.common.data.id.DeviceProfileId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
|
||||
@ -30,34 +31,44 @@ public class DeviceProfileCacheKey implements Serializable {
|
||||
private final String name;
|
||||
private final DeviceProfileId deviceProfileId;
|
||||
private final boolean defaultProfile;
|
||||
private final String provisionDeviceKey;
|
||||
|
||||
private DeviceProfileCacheKey(TenantId tenantId, String name, DeviceProfileId deviceProfileId, boolean defaultProfile) {
|
||||
private DeviceProfileCacheKey(TenantId tenantId, String name, DeviceProfileId deviceProfileId, boolean defaultProfile, String provisionDeviceKey) {
|
||||
this.tenantId = tenantId;
|
||||
this.name = name;
|
||||
this.deviceProfileId = deviceProfileId;
|
||||
this.defaultProfile = defaultProfile;
|
||||
this.provisionDeviceKey = provisionDeviceKey;
|
||||
}
|
||||
|
||||
public static DeviceProfileCacheKey fromName(TenantId tenantId, String name) {
|
||||
return new DeviceProfileCacheKey(tenantId, name, null, false);
|
||||
return new DeviceProfileCacheKey(tenantId, name, null, false, null);
|
||||
}
|
||||
|
||||
public static DeviceProfileCacheKey fromId(DeviceProfileId id) {
|
||||
return new DeviceProfileCacheKey(null, null, id, false);
|
||||
return new DeviceProfileCacheKey(null, null, id, false, null);
|
||||
}
|
||||
|
||||
public static DeviceProfileCacheKey defaultProfile(TenantId tenantId) {
|
||||
return new DeviceProfileCacheKey(tenantId, null, null, true);
|
||||
return new DeviceProfileCacheKey(tenantId, null, null, true, null);
|
||||
}
|
||||
|
||||
public static DeviceProfileCacheKey fromProvisionDeviceKey(String provisionDeviceKey) {
|
||||
return new DeviceProfileCacheKey(null, null, null, false, provisionDeviceKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* IMPORTANT: Method toString() has to return unique value, if you add additional field to this class, please also refactor toString().
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
if (deviceProfileId != null) {
|
||||
return deviceProfileId.toString();
|
||||
} else if (defaultProfile) {
|
||||
return tenantId.toString();
|
||||
} else {
|
||||
return tenantId + "_" + name;
|
||||
} else if (StringUtils.isNotEmpty(provisionDeviceKey)) {
|
||||
return provisionDeviceKey;
|
||||
}
|
||||
return tenantId + "_" + name;
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,5 +27,6 @@ public class DeviceProfileEvictEvent {
|
||||
private final String oldName;
|
||||
private final DeviceProfileId deviceProfileId;
|
||||
private final boolean defaultProfile;
|
||||
private final String provisionDeviceKey;
|
||||
|
||||
}
|
||||
|
||||
@ -34,12 +34,14 @@ import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileCon
|
||||
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration;
|
||||
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
|
||||
import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration;
|
||||
import org.thingsboard.server.common.data.device.profile.X509CertificateChainProvisionConfiguration;
|
||||
import org.thingsboard.server.common.data.id.DeviceProfileId;
|
||||
import org.thingsboard.server.common.data.id.EntityId;
|
||||
import org.thingsboard.server.common.data.id.HasId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.common.data.page.PageData;
|
||||
import org.thingsboard.server.common.data.page.PageLink;
|
||||
import org.thingsboard.server.common.msg.EncryptionUtil;
|
||||
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
|
||||
import org.thingsboard.server.dao.exception.DataValidationException;
|
||||
import org.thingsboard.server.dao.queue.QueueService;
|
||||
@ -47,12 +49,19 @@ import org.thingsboard.server.dao.service.DataValidator;
|
||||
import org.thingsboard.server.dao.service.PaginatedRemover;
|
||||
import org.thingsboard.server.dao.service.Validator;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.thingsboard.server.dao.service.Validator.validateId;
|
||||
import static org.thingsboard.server.dao.service.Validator.validateString;
|
||||
|
||||
@Service("DeviceProfileDaoService")
|
||||
@Slf4j
|
||||
@ -61,6 +70,7 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
|
||||
private static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
|
||||
private static final String INCORRECT_DEVICE_PROFILE_ID = "Incorrect deviceProfileId ";
|
||||
private static final String INCORRECT_DEVICE_PROFILE_NAME = "Incorrect deviceProfileName ";
|
||||
private static final String INCORRECT_PROVISION_DEVICE_KEY = "Incorrect provisionDeviceKey ";
|
||||
private static final String DEVICE_PROFILE_WITH_SUCH_NAME_ALREADY_EXISTS = "Device profile with such name already exists!";
|
||||
|
||||
@Autowired
|
||||
@ -93,13 +103,16 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
|
||||
if (StringUtils.isNotEmpty(event.getOldName()) && !event.getOldName().equals(event.getNewName())) {
|
||||
keys.add(DeviceProfileCacheKey.fromName(event.getTenantId(), event.getOldName()));
|
||||
}
|
||||
if (StringUtils.isNotEmpty(event.getProvisionDeviceKey())) {
|
||||
keys.add(DeviceProfileCacheKey.fromProvisionDeviceKey(event.getProvisionDeviceKey()));
|
||||
}
|
||||
cache.evict(keys);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceProfile findDeviceProfileById(TenantId tenantId, DeviceProfileId deviceProfileId) {
|
||||
log.trace("Executing findDeviceProfileById [{}]", deviceProfileId);
|
||||
Validator.validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId);
|
||||
validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId);
|
||||
return cache.getAndPutInTransaction(DeviceProfileCacheKey.fromId(deviceProfileId),
|
||||
() -> deviceProfileDao.findById(tenantId, deviceProfileId.getId()), true);
|
||||
}
|
||||
@ -107,30 +120,46 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
|
||||
@Override
|
||||
public DeviceProfile findDeviceProfileByName(TenantId tenantId, String profileName) {
|
||||
log.trace("Executing findDeviceProfileByName [{}][{}]", tenantId, profileName);
|
||||
Validator.validateString(profileName, INCORRECT_DEVICE_PROFILE_NAME + profileName);
|
||||
validateString(profileName, INCORRECT_DEVICE_PROFILE_NAME + profileName);
|
||||
return cache.getAndPutInTransaction(DeviceProfileCacheKey.fromName(tenantId, profileName),
|
||||
() -> deviceProfileDao.findByName(tenantId, profileName), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceProfile findDeviceProfileByProvisionDeviceKey(String provisionDeviceKey) {
|
||||
log.trace("Executing findDeviceProfileByProvisionDeviceKey provisionKey [{}]", provisionDeviceKey);
|
||||
validateString(provisionDeviceKey, INCORRECT_PROVISION_DEVICE_KEY + provisionDeviceKey);
|
||||
return cache.getAndPutInTransaction(DeviceProfileCacheKey.fromProvisionDeviceKey(provisionDeviceKey),
|
||||
() -> deviceProfileDao.findByProvisionDeviceKey(provisionDeviceKey), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceProfileInfo findDeviceProfileInfoById(TenantId tenantId, DeviceProfileId deviceProfileId) {
|
||||
log.trace("Executing findDeviceProfileById [{}]", deviceProfileId);
|
||||
Validator.validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId);
|
||||
validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId);
|
||||
return toDeviceProfileInfo(findDeviceProfileById(tenantId, deviceProfileId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceProfile saveDeviceProfile(DeviceProfile deviceProfile) {
|
||||
log.trace("Executing saveDeviceProfile [{}]", deviceProfile);
|
||||
if (deviceProfile.getProfileData() != null && deviceProfile.getProfileData().getProvisionConfiguration() instanceof X509CertificateChainProvisionConfiguration) {
|
||||
X509CertificateChainProvisionConfiguration x509Configuration = (X509CertificateChainProvisionConfiguration) deviceProfile.getProfileData().getProvisionConfiguration();
|
||||
if (x509Configuration.getProvisionDeviceSecret() != null) {
|
||||
formatDeviceProfileCertificate(deviceProfile, x509Configuration);
|
||||
}
|
||||
}
|
||||
DeviceProfile oldDeviceProfile = deviceProfileValidator.validate(deviceProfile, DeviceProfile::getTenantId);
|
||||
DeviceProfile savedDeviceProfile;
|
||||
try {
|
||||
savedDeviceProfile = deviceProfileDao.saveAndFlush(deviceProfile.getTenantId(), deviceProfile);
|
||||
publishEvictEvent(new DeviceProfileEvictEvent(savedDeviceProfile.getTenantId(), savedDeviceProfile.getName(),
|
||||
oldDeviceProfile != null ? oldDeviceProfile.getName() : null, savedDeviceProfile.getId(), savedDeviceProfile.isDefault()));
|
||||
oldDeviceProfile != null ? oldDeviceProfile.getName() : null, savedDeviceProfile.getId(), savedDeviceProfile.isDefault(),
|
||||
oldDeviceProfile != null ? oldDeviceProfile.getProvisionDeviceKey() : null));
|
||||
} catch (Exception t) {
|
||||
handleEvictEvent(new DeviceProfileEvictEvent(deviceProfile.getTenantId(), deviceProfile.getName(),
|
||||
oldDeviceProfile != null ? oldDeviceProfile.getName() : null, null, deviceProfile.isDefault()));
|
||||
oldDeviceProfile != null ? oldDeviceProfile.getName() : null, null, deviceProfile.isDefault(),
|
||||
oldDeviceProfile != null ? oldDeviceProfile.getProvisionDeviceKey() : null));
|
||||
checkConstraintViolation(t,
|
||||
Map.of("device_profile_name_unq_key", DEVICE_PROFILE_WITH_SUCH_NAME_ALREADY_EXISTS,
|
||||
"device_provision_key_unq_key", "Device profile with such provision device key already exists!",
|
||||
@ -156,7 +185,7 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
|
||||
@Transactional
|
||||
public void deleteDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId) {
|
||||
log.trace("Executing deleteDeviceProfile [{}]", deviceProfileId);
|
||||
Validator.validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId);
|
||||
validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId);
|
||||
DeviceProfile deviceProfile = deviceProfileDao.findById(tenantId, deviceProfileId.getId());
|
||||
if (deviceProfile != null && deviceProfile.isDefault()) {
|
||||
throw new DataValidationException("Deletion of Default Device Profile is prohibited!");
|
||||
@ -170,7 +199,8 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
|
||||
deleteEntityRelations(tenantId, deviceProfileId);
|
||||
deviceProfileDao.removeById(tenantId, deviceProfileId.getId());
|
||||
publishEvictEvent(new DeviceProfileEvictEvent(deviceProfile.getTenantId(), deviceProfile.getName(),
|
||||
null, deviceProfile.getId(), deviceProfile.isDefault()));
|
||||
null, deviceProfile.getId(), deviceProfile.isDefault(),
|
||||
deviceProfile.getProvisionDeviceKey()));
|
||||
} catch (Exception t) {
|
||||
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
|
||||
if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_device_profile")) {
|
||||
@ -260,7 +290,7 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
|
||||
@Override
|
||||
public boolean setDefaultDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId) {
|
||||
log.trace("Executing setDefaultDeviceProfile [{}]", deviceProfileId);
|
||||
Validator.validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId);
|
||||
validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId);
|
||||
DeviceProfile deviceProfile = deviceProfileDao.findById(tenantId, deviceProfileId.getId());
|
||||
if (!deviceProfile.isDefault()) {
|
||||
deviceProfile.setDefault(true);
|
||||
@ -268,14 +298,14 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
|
||||
boolean changed = false;
|
||||
if (previousDefaultDeviceProfile == null) {
|
||||
deviceProfileDao.save(tenantId, deviceProfile);
|
||||
publishEvictEvent(new DeviceProfileEvictEvent(deviceProfile.getTenantId(), deviceProfile.getName(), null, deviceProfile.getId(), true));
|
||||
publishEvictEvent(new DeviceProfileEvictEvent(deviceProfile.getTenantId(), deviceProfile.getName(), null, deviceProfile.getId(), true, deviceProfile.getProvisionDeviceKey()));
|
||||
changed = true;
|
||||
} else if (!previousDefaultDeviceProfile.getId().equals(deviceProfile.getId())) {
|
||||
previousDefaultDeviceProfile.setDefault(false);
|
||||
deviceProfileDao.save(tenantId, previousDefaultDeviceProfile);
|
||||
deviceProfileDao.save(tenantId, deviceProfile);
|
||||
publishEvictEvent(new DeviceProfileEvictEvent(previousDefaultDeviceProfile.getTenantId(), previousDefaultDeviceProfile.getName(), null, previousDefaultDeviceProfile.getId(), false));
|
||||
publishEvictEvent(new DeviceProfileEvictEvent(deviceProfile.getTenantId(), deviceProfile.getName(), null, deviceProfile.getId(), true));
|
||||
publishEvictEvent(new DeviceProfileEvictEvent(previousDefaultDeviceProfile.getTenantId(), previousDefaultDeviceProfile.getName(), null, previousDefaultDeviceProfile.getId(), false, deviceProfile.getProvisionDeviceKey()));
|
||||
publishEvictEvent(new DeviceProfileEvictEvent(deviceProfile.getTenantId(), deviceProfile.getName(), null, deviceProfile.getId(), true, deviceProfile.getProvisionDeviceKey()));
|
||||
changed = true;
|
||||
}
|
||||
return changed;
|
||||
@ -319,4 +349,38 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
|
||||
profile.getDefaultDashboardId(), profile.getType(), profile.getTransportType());
|
||||
}
|
||||
|
||||
private void formatDeviceProfileCertificate(DeviceProfile deviceProfile, X509CertificateChainProvisionConfiguration x509Configuration) {
|
||||
String formattedCertificateValue = formatCertificateValue(x509Configuration.getProvisionDeviceSecret());
|
||||
String cert = fetchLeafCertificateFromChain(formattedCertificateValue);
|
||||
String sha3Hash = EncryptionUtil.getSha3Hash(cert);
|
||||
DeviceProfileData deviceProfileData = deviceProfile.getProfileData();
|
||||
x509Configuration.setProvisionDeviceSecret(formattedCertificateValue);
|
||||
deviceProfileData.setProvisionConfiguration(x509Configuration);
|
||||
deviceProfile.setProfileData(deviceProfileData);
|
||||
deviceProfile.setProvisionDeviceKey(sha3Hash);
|
||||
}
|
||||
|
||||
private String fetchLeafCertificateFromChain(String value) {
|
||||
String regex = "-----BEGIN CERTIFICATE-----\\s*.*?\\s*-----END CERTIFICATE-----";
|
||||
Pattern pattern = Pattern.compile(regex);
|
||||
Matcher matcher = pattern.matcher(value);
|
||||
if (matcher.find()) {
|
||||
// if the method receives a chain it fetches the leaf (end-entity) certificate, else if it gets a single certificate, it returns the single certificate
|
||||
return matcher.group(0);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private String formatCertificateValue(String certificateValue) {
|
||||
try {
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(certificateValue.getBytes());
|
||||
Certificate[] certificates = cf.generateCertificates(inputStream).toArray(new Certificate[0]);
|
||||
if (certificates.length > 1) {
|
||||
return EncryptionUtil.certTrimNewLinesForChainInDeviceProfile(certificateValue);
|
||||
}
|
||||
} catch (CertificateException ignored) {}
|
||||
return EncryptionUtil.certTrimNewLines(certificateValue);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -19,8 +19,10 @@ import com.google.protobuf.Descriptors;
|
||||
import com.google.protobuf.DynamicMessage;
|
||||
import org.eclipse.leshan.core.util.SecurityUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Base64Utils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.thingsboard.server.common.data.DashboardInfo;
|
||||
import org.thingsboard.server.common.data.DeviceProfile;
|
||||
@ -55,6 +57,17 @@ import org.thingsboard.server.dao.queue.QueueService;
|
||||
import org.thingsboard.server.dao.rule.RuleChainService;
|
||||
import org.thingsboard.server.dao.tenant.TenantService;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.PKIXParameters;
|
||||
import java.security.cert.TrustAnchor;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@ -85,6 +98,12 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator<D
|
||||
@Autowired
|
||||
private DashboardService dashboardService;
|
||||
|
||||
@Value("${security.java_cacerts.path}")
|
||||
private String javaCacertsPath;
|
||||
|
||||
@Value("${security.java_cacerts.password}")
|
||||
private String javaCacertsPassword;
|
||||
|
||||
@Override
|
||||
protected void validateDataImpl(TenantId tenantId, DeviceProfile deviceProfile) {
|
||||
if (StringUtils.isEmpty(deviceProfile.getName())) {
|
||||
@ -118,6 +137,11 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator<D
|
||||
if (deviceProfile.getProvisionType() == null) {
|
||||
deviceProfile.setProvisionType(DeviceProfileProvisionType.DISABLED);
|
||||
}
|
||||
if (deviceProfile.getProvisionDeviceKey() != null && DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN.equals(deviceProfile.getProvisionType())) {
|
||||
if (isDeviceProfileCertificateInJavaCacerts(deviceProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret())) {
|
||||
throw new DataValidationException("Device profile certificate cannot be well known root CA!");
|
||||
}
|
||||
}
|
||||
DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration();
|
||||
transportConfiguration.validate();
|
||||
if (transportConfiguration instanceof MqttDeviceProfileTransportConfiguration) {
|
||||
@ -211,6 +235,11 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator<D
|
||||
throw new DataValidationException(message);
|
||||
}
|
||||
}
|
||||
if (deviceProfile.getProvisionDeviceKey() != null && DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN.equals(deviceProfile.getProvisionType())) {
|
||||
if (isDeviceProfileCertificateInJavaCacerts(deviceProfile.getProvisionDeviceKey())) {
|
||||
throw new DataValidationException("Device profile certificate cannot be well known root CA!");
|
||||
}
|
||||
}
|
||||
return old;
|
||||
}
|
||||
|
||||
@ -363,4 +392,27 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator<D
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isDeviceProfileCertificateInJavaCacerts(String deviceProfileX509Secret) {
|
||||
try {
|
||||
FileInputStream is = new FileInputStream(javaCacertsPath);
|
||||
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
keystore.load(is, javaCacertsPassword.toCharArray());
|
||||
|
||||
PKIXParameters params = new PKIXParameters(keystore);
|
||||
for (TrustAnchor ta : params.getTrustAnchors()) {
|
||||
X509Certificate cert = ta.getTrustedCert();
|
||||
if (getCertificateString(cert).equals(deviceProfileX509Secret)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (CertificateException | KeyStoreException | NoSuchAlgorithmException |
|
||||
InvalidAlgorithmParameterException | IOException ignored) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String getCertificateString(X509Certificate cert) throws CertificateEncodingException {
|
||||
return EncryptionUtil.certTrimNewLines(Base64Utils.encodeToString(cert.getEncoded()));
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,6 +82,8 @@ redis.connection.password=
|
||||
security.user_login_case_sensitive=true
|
||||
security.claim.allowClaimingByDefault=true
|
||||
security.claim.duration=60000
|
||||
security.java_cacerts.path=/path/to/cacerts/file
|
||||
security.java_cacerts.password=myPassword
|
||||
|
||||
database.ts_max_intervals=700
|
||||
|
||||
|
||||
@ -114,7 +114,7 @@ transport:
|
||||
# Server SSL credentials
|
||||
credentials:
|
||||
# Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore)
|
||||
type: "${MQTT_SSL_CREDENTIALS_TYPE:PEM}"
|
||||
type: "${MQTT_SSL_CREDENTIALS_TYPE:PEM}"
|
||||
# PEM server credentials
|
||||
pem:
|
||||
# Path to the server certificate file (holds server certificate or certificate chain, may include server private key)
|
||||
|
||||
@ -27,36 +27,80 @@
|
||||
{{ 'device-profile.provision-strategy-required' | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<section *ngIf="provisionConfigurationFormGroup.get('type').value !== deviceProvisionType.DISABLED" fxLayoutGap.gt-xs="8px" fxLayout="row" fxLayout.xs="column">
|
||||
<mat-form-field fxFlex class="mat-block">
|
||||
<mat-label translate>device-profile.provision-device-key</mat-label>
|
||||
<input matInput formControlName="provisionDeviceKey" required/>
|
||||
<button matSuffix mat-icon-button
|
||||
ngxClipboard
|
||||
[cbContent]="provisionConfigurationFormGroup.get('provisionDeviceKey').value"
|
||||
(cbOnSuccess)="onProvisionCopied(true)"
|
||||
matTooltip="{{ 'device-profile.copy-provision-key' | translate }}"
|
||||
matTooltipPosition="above">
|
||||
<mat-icon svgIcon="mdi:clipboard-arrow-left" style="font-size: 20px;"></mat-icon>
|
||||
</button>
|
||||
<mat-error *ngIf="provisionConfigurationFormGroup.get('provisionDeviceKey').hasError('required')">
|
||||
{{ 'device-profile.provision-device-key-required' | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field fxFlex class="mat-block">
|
||||
<mat-label translate>device-profile.provision-device-secret</mat-label>
|
||||
<input matInput formControlName="provisionDeviceSecret" required/>
|
||||
<button matSuffix mat-icon-button
|
||||
ngxClipboard
|
||||
[cbContent]="provisionConfigurationFormGroup.get('provisionDeviceSecret').value"
|
||||
(cbOnSuccess)="onProvisionCopied(false)"
|
||||
matTooltip="{{ 'device-profile.copy-provision-secret' | translate }}"
|
||||
matTooltipPosition="above">
|
||||
<mat-icon svgIcon="mdi:clipboard-arrow-left" style="font-size: 20px;"></mat-icon>
|
||||
</button>
|
||||
<mat-error *ngIf="provisionConfigurationFormGroup.get('provisionDeviceSecret').hasError('required')">
|
||||
{{ 'device-profile.provision-device-secret-required' | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
<div [ngSwitch]="provisionConfigurationFormGroup.get('type').value">
|
||||
<ng-template [ngSwitchCase]="deviceProvisionType.ALLOW_CREATE_NEW_DEVICES">
|
||||
<ng-container *ngTemplateOutlet="default"></ng-container>
|
||||
</ng-template>
|
||||
<ng-template [ngSwitchCase]="deviceProvisionType.CHECK_PRE_PROVISIONED_DEVICES">
|
||||
<ng-container *ngTemplateOutlet="default"></ng-container>
|
||||
</ng-template>
|
||||
<ng-template [ngSwitchCase]="deviceProvisionType.X509_CERTIFICATE_CHAIN">
|
||||
<div fxFlex fxLayoutAlign="start center" class="tb-hint">
|
||||
<span [innerHTML]="'device-profile.provision-strategy-x509.certificate-chain-hint' | translate"></span>
|
||||
<span tb-help-popup="device-profile/x509-chain-hint"
|
||||
tb-help-popup-placement="top"
|
||||
trigger-style="letter-spacing:0.25px; font-size: 12px"
|
||||
[tb-help-popup-style]="{maxWidth: '820px'}"
|
||||
trigger-text="{{ 'action.more' | translate }}"></span>
|
||||
</div>
|
||||
<mat-slide-toggle formControlName="allowCreateNewDevicesByX509Certificate">
|
||||
{{ 'device-profile.provision-strategy-x509.allow-create-new-devices' | translate }}
|
||||
</mat-slide-toggle>
|
||||
<div class="tb-hint" style="padding:0 40px 16px" [innerHTML]="'device-profile.provision-strategy-x509.allow-create-new-devices-hint' | translate"></div>
|
||||
<mat-form-field class="mat-block">
|
||||
<mat-label translate>device-profile.provision-strategy-x509.certificate-value</mat-label>
|
||||
<textarea matInput formControlName="certificateValue" cols="15" rows="5" required></textarea>
|
||||
<mat-error *ngIf="provisionConfigurationFormGroup.get('certificateValue').hasError('required')">
|
||||
{{ 'device-profile.provision-strategy-x509.certificate-value-required' | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="mat-block">
|
||||
<mat-label translate>device-profile.provision-strategy-x509.cn-regex-variable</mat-label>
|
||||
<input matInput type="text" formControlName="certificateRegExPattern" required>
|
||||
<span matSuffix
|
||||
tb-help-popup="device-profile/x509-chain-regex-examples"
|
||||
tb-help-popup-placement="top"
|
||||
trigger-style="letter-spacing:0.25px"
|
||||
[tb-help-popup-style]="{maxWidth: '820px'}"></span>
|
||||
<mat-error *ngIf="provisionConfigurationFormGroup.get('certificateRegExPattern').hasError('required')">
|
||||
{{ 'device-profile.provision-strategy-x509.cn-regex-variable-required' | translate }}
|
||||
</mat-error>
|
||||
<mat-hint translate>device-profile.provision-strategy-x509.cn-regex-variable-hint</mat-hint>
|
||||
</mat-form-field>
|
||||
</ng-template>
|
||||
<ng-template #default>
|
||||
<section fxLayoutGap.gt-xs="8px" fxLayout="row" fxLayout.xs="column">
|
||||
<mat-form-field fxFlex class="mat-block">
|
||||
<mat-label translate>device-profile.provision-device-key</mat-label>
|
||||
<input matInput formControlName="provisionDeviceKey" required/>
|
||||
<button matSuffix mat-icon-button
|
||||
ngxClipboard
|
||||
[cbContent]="provisionConfigurationFormGroup.get('provisionDeviceKey').value"
|
||||
(cbOnSuccess)="onProvisionCopied(true)"
|
||||
matTooltip="{{ 'device-profile.copy-provision-key' | translate }}"
|
||||
matTooltipPosition="above">
|
||||
<mat-icon svgIcon="mdi:clipboard-arrow-left" style="font-size: 20px;"></mat-icon>
|
||||
</button>
|
||||
<mat-error *ngIf="provisionConfigurationFormGroup.get('provisionDeviceKey').hasError('required')">
|
||||
{{ 'device-profile.provision-device-key-required' | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field fxFlex class="mat-block">
|
||||
<mat-label translate>device-profile.provision-device-secret</mat-label>
|
||||
<input matInput formControlName="provisionDeviceSecret" required/>
|
||||
<button matSuffix mat-icon-button
|
||||
ngxClipboard
|
||||
[cbContent]="provisionConfigurationFormGroup.get('provisionDeviceSecret').value"
|
||||
(cbOnSuccess)="onProvisionCopied(false)"
|
||||
matTooltip="{{ 'device-profile.copy-provision-secret' | translate }}"
|
||||
matTooltipPosition="above">
|
||||
<mat-icon svgIcon="mdi:clipboard-arrow-left" style="font-size: 20px;"></mat-icon>
|
||||
</button>
|
||||
<mat-error *ngIf="provisionConfigurationFormGroup.get('provisionDeviceSecret').hasError('required')">
|
||||
{{ 'device-profile.provision-device-secret-required' | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -32,7 +32,7 @@ import {
|
||||
DeviceProvisionType,
|
||||
deviceProvisionTypeTranslationMap
|
||||
} from '@shared/models/device.models';
|
||||
import { generateSecret, isDefinedAndNotNull } from '@core/utils';
|
||||
import { generateSecret, isBoolean, isDefinedAndNotNull } from '@core/utils';
|
||||
import { ActionNotificationShow } from '@core/notification/notification.actions';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppState } from '@core/core.state';
|
||||
@ -86,14 +86,36 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu
|
||||
this.provisionConfigurationFormGroup = this.fb.group({
|
||||
type: [DeviceProvisionType.DISABLED, Validators.required],
|
||||
provisionDeviceSecret: [{value: null, disabled: true}, Validators.required],
|
||||
provisionDeviceKey: [{value: null, disabled: true}, Validators.required]
|
||||
provisionDeviceKey: [{value: null, disabled: true}, Validators.required],
|
||||
certificateValue: [{value: null, disabled: true}, Validators.required],
|
||||
certificateRegExPattern: [{value: null, disabled: true}, Validators.required],
|
||||
allowCreateNewDevicesByX509Certificate: [{value: null, disabled: true}, Validators.required]
|
||||
});
|
||||
this.provisionConfigurationFormGroup.get('type').valueChanges.subscribe((type) => {
|
||||
if (type === DeviceProvisionType.DISABLED) {
|
||||
this.provisionConfigurationFormGroup.get('provisionDeviceSecret').disable({emitEvent: false});
|
||||
this.provisionConfigurationFormGroup.get('provisionDeviceSecret').patchValue(null, {emitEvent: false});
|
||||
this.provisionConfigurationFormGroup.get('provisionDeviceKey').disable({emitEvent: false});
|
||||
this.provisionConfigurationFormGroup.get('provisionDeviceKey').patchValue(null);
|
||||
for (const field in this.provisionConfigurationFormGroup.controls) {
|
||||
if (field !== 'type') {
|
||||
const control = this.provisionConfigurationFormGroup.get(field);
|
||||
control.disable({emitEvent: false});
|
||||
control.patchValue(null, {emitEvent: false});
|
||||
}
|
||||
}
|
||||
} else if (type === DeviceProvisionType.X509_CERTIFICATE_CHAIN) {
|
||||
const certificateValue: string = this.provisionConfigurationFormGroup.get('certificateValue').value;
|
||||
if (!certificateValue || !certificateValue.length) {
|
||||
this.provisionConfigurationFormGroup.get('certificateValue').patchValue(null, {emitEvent: false});
|
||||
}
|
||||
const certificateRegExPattern: string = this.provisionConfigurationFormGroup.get('certificateRegExPattern').value;
|
||||
if (!certificateRegExPattern || !certificateRegExPattern.length) {
|
||||
this.provisionConfigurationFormGroup.get('certificateRegExPattern').patchValue('[\\w]*', {emitEvent: false});
|
||||
}
|
||||
const allowCreateNewDevicesByX509Certificate: boolean | null = this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').value;
|
||||
if (!isBoolean(allowCreateNewDevicesByX509Certificate)) {
|
||||
this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').patchValue(true, {emitEvent: false});
|
||||
}
|
||||
this.provisionConfigurationFormGroup.get('certificateValue').enable({emitEvent: false});
|
||||
this.provisionConfigurationFormGroup.get('certificateRegExPattern').enable({emitEvent: false});
|
||||
this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').enable({emitEvent: false});
|
||||
} else {
|
||||
const provisionDeviceSecret: string = this.provisionConfigurationFormGroup.get('provisionDeviceSecret').value;
|
||||
if (!provisionDeviceSecret || !provisionDeviceSecret.length) {
|
||||
@ -120,16 +142,19 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu
|
||||
}
|
||||
|
||||
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});
|
||||
} else {
|
||||
this.provisionConfigurationFormGroup.patchValue({type: DeviceProvisionType.DISABLED});
|
||||
}
|
||||
}
|
||||
|
||||
setDisabledState(isDisabled: boolean){
|
||||
setDisabledState(isDisabled: boolean) {
|
||||
this.disabled = isDisabled;
|
||||
if (this.disabled){
|
||||
if (this.disabled) {
|
||||
this.provisionConfigurationFormGroup.disable({emitEvent: false});
|
||||
} else {
|
||||
if (this.provisionConfigurationFormGroup.get('type').value !== DeviceProvisionType.DISABLED) {
|
||||
@ -150,8 +175,12 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu
|
||||
|
||||
private updateModel(): void {
|
||||
let deviceProvisionConfiguration: DeviceProvisionConfiguration = null;
|
||||
this.resetFormControls(this.provisionConfigurationFormGroup.value);
|
||||
if (this.provisionConfigurationFormGroup.valid) {
|
||||
deviceProvisionConfiguration = this.provisionConfigurationFormGroup.getRawValue();
|
||||
if (deviceProvisionConfiguration.type === DeviceProvisionType.X509_CERTIFICATE_CHAIN) {
|
||||
deviceProvisionConfiguration.provisionDeviceSecret = deviceProvisionConfiguration.certificateValue;
|
||||
}
|
||||
}
|
||||
this.propagateChange(deviceProvisionConfiguration);
|
||||
}
|
||||
@ -166,4 +195,15 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu
|
||||
horizontalPosition: 'right'
|
||||
}));
|
||||
}
|
||||
|
||||
private resetFormControls(value: DeviceProvisionConfiguration) {
|
||||
if (value.type === DeviceProvisionType.CHECK_PRE_PROVISIONED_DEVICES || value.type === DeviceProvisionType.ALLOW_CREATE_NEW_DEVICES) {
|
||||
this.provisionConfigurationFormGroup.get('certificateValue').reset({value: null, disabled: true}, {emitEvent: false});
|
||||
this.provisionConfigurationFormGroup.get('certificateRegExPattern').reset({value: null, disabled: true}, {emitEvent: false});
|
||||
this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').reset({value: null, disabled: true}, {emitEvent: false});
|
||||
} else if (value.type === DeviceProvisionType.X509_CERTIFICATE_CHAIN) {
|
||||
this.provisionConfigurationFormGroup.get('provisionDeviceSecret').reset({value: null, disabled: true}, {emitEvent: false});
|
||||
this.provisionConfigurationFormGroup.get('provisionDeviceKey').reset({value: null, disabled: true}, {emitEvent: false});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,7 +104,9 @@ export class DeviceProfileComponent extends EntityComponent<DeviceProfile> {
|
||||
const deviceProvisionConfiguration: DeviceProvisionConfiguration = {
|
||||
type: entity?.provisionType ? entity.provisionType : DeviceProvisionType.DISABLED,
|
||||
provisionDeviceKey: entity?.provisionDeviceKey,
|
||||
provisionDeviceSecret: entity?.profileData?.provisionConfiguration?.provisionDeviceSecret
|
||||
provisionDeviceSecret: entity?.profileData?.provisionConfiguration?.provisionDeviceSecret,
|
||||
certificateRegExPattern: entity?.profileData?.provisionConfiguration?.certificateRegExPattern,
|
||||
allowCreateNewDevicesByX509Certificate: entity?.profileData?.provisionConfiguration?.allowCreateNewDevicesByX509Certificate
|
||||
};
|
||||
const form = this.fb.group(
|
||||
{
|
||||
@ -185,7 +187,9 @@ export class DeviceProfileComponent extends EntityComponent<DeviceProfile> {
|
||||
const deviceProvisionConfiguration: DeviceProvisionConfiguration = {
|
||||
type: entity?.provisionType ? entity.provisionType : DeviceProvisionType.DISABLED,
|
||||
provisionDeviceKey: entity?.provisionDeviceKey,
|
||||
provisionDeviceSecret: entity?.profileData?.provisionConfiguration?.provisionDeviceSecret
|
||||
provisionDeviceSecret: entity?.profileData?.provisionConfiguration?.provisionDeviceSecret,
|
||||
certificateRegExPattern: entity?.profileData?.provisionConfiguration?.certificateRegExPattern,
|
||||
allowCreateNewDevicesByX509Certificate: entity?.profileData?.provisionConfiguration?.allowCreateNewDevicesByX509Certificate
|
||||
};
|
||||
this.entityForm.patchValue({name: entity.name});
|
||||
this.entityForm.patchValue({type: entity.type}, {emitEvent: false});
|
||||
|
||||
@ -169,6 +169,6 @@
|
||||
<mat-checkbox formControlName="sendAckOnValidationException">
|
||||
{{ 'device-profile.mqtt-send-ack-on-validation-exception' | translate }}
|
||||
</mat-checkbox>
|
||||
<div class="tb-hint" innerHTML="{{ 'device-profile.mqtt-send-ack-on-validation-exception-hint' | translate }}"></div>
|
||||
<div class="tb-hint" style="max-width: 800px" innerHTML="{{ 'device-profile.mqtt-send-ack-on-validation-exception-hint' | translate }}"></div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -62,7 +62,8 @@ export enum CoapTransportDeviceType {
|
||||
export enum DeviceProvisionType {
|
||||
DISABLED = 'DISABLED',
|
||||
ALLOW_CREATE_NEW_DEVICES = 'ALLOW_CREATE_NEW_DEVICES',
|
||||
CHECK_PRE_PROVISIONED_DEVICES = 'CHECK_PRE_PROVISIONED_DEVICES'
|
||||
CHECK_PRE_PROVISIONED_DEVICES = 'CHECK_PRE_PROVISIONED_DEVICES',
|
||||
X509_CERTIFICATE_CHAIN = 'X509_CERTIFICATE_CHAIN'
|
||||
}
|
||||
|
||||
export interface DeviceConfigurationFormInfo {
|
||||
@ -110,7 +111,8 @@ export const deviceProvisionTypeTranslationMap = new Map<DeviceProvisionType, st
|
||||
[
|
||||
[DeviceProvisionType.DISABLED, 'device-profile.provision-strategy-disabled'],
|
||||
[DeviceProvisionType.ALLOW_CREATE_NEW_DEVICES, 'device-profile.provision-strategy-created-new'],
|
||||
[DeviceProvisionType.CHECK_PRE_PROVISIONED_DEVICES, 'device-profile.provision-strategy-check-pre-provisioned']
|
||||
[DeviceProvisionType.CHECK_PRE_PROVISIONED_DEVICES, 'device-profile.provision-strategy-check-pre-provisioned'],
|
||||
[DeviceProvisionType.X509_CERTIFICATE_CHAIN, 'device-profile.provision-strategy-x509.certificate-chain']
|
||||
]
|
||||
);
|
||||
|
||||
@ -320,6 +322,9 @@ export interface DeviceProvisionConfiguration {
|
||||
type: DeviceProvisionType;
|
||||
provisionDeviceSecret?: string;
|
||||
provisionDeviceKey?: string;
|
||||
certificateValue?: string;
|
||||
certificateRegExPattern?: string;
|
||||
allowCreateNewDevicesByX509Certificate?: boolean;
|
||||
}
|
||||
|
||||
export function createDeviceProfileConfiguration(type: DeviceProfileType): DeviceProfileConfiguration {
|
||||
|
||||
@ -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)*.
|
||||
@ -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.
|
||||
@ -63,6 +63,8 @@
|
||||
"print": "Print",
|
||||
"restore": "Restore",
|
||||
"confirm": "Confirm",
|
||||
"more": "More",
|
||||
"less": "Less",
|
||||
"skip": "Skip",
|
||||
"send": "Send"
|
||||
},
|
||||
@ -1516,6 +1518,17 @@
|
||||
"provision-device-secret-required": "Provision device secret is required.",
|
||||
"copy-provision-secret": "Copy provision secret",
|
||||
"provision-secret-copied-message": "Provision secret has been copied to clipboard",
|
||||
"provision-strategy-x509": {
|
||||
"certificate-chain": "X509 Certificates Chain",
|
||||
"certificate-chain-hint": "X.509 certificates strategy is used to provision devices by client certificates in two-way TLS communication.",
|
||||
"allow-create-new-devices": "Create new devices",
|
||||
"allow-create-new-devices-hint": "If selected new devices will be created and client certificate will be used as device credentials.",
|
||||
"certificate-value": "Certificate in PEM format",
|
||||
"certificate-value-required": "Certificate in PEM format is required",
|
||||
"cn-regex-variable": "CN Regular Expression variable",
|
||||
"cn-regex-variable-required": "CN Regular Expression variable is required",
|
||||
"cn-regex-variable-hint": "Required to fetch device name from device's X509 certificate's common name."
|
||||
},
|
||||
"condition": "Condition",
|
||||
"condition-type": "Condition type",
|
||||
"condition-type-simple": "Simple",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user