diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 6ac41979d6..ae22aaca51 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -583,7 +583,24 @@ transport: bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}" bind_port: "${COAP_BIND_PORT:5683}" timeout: "${COAP_TIMEOUT:10000}" - + dtls: + # Enable/disable DTLS 1.2 support + enabled: "${COAP_DTLS_ENABLED:false}" + # Secure mode. Allowed values: NO_AUTH, X509 + mode: "${COAP_DTLS_SECURE_MODE:NO_AUTH}" + # Path to the key store that holds the certificate + key_store: "${COAP_DTLS_KEY_STORE:coapserver.jks}" + # Password used to access the key store + key_store_password: "${COAP_DTLS_KEY_STORE_PASSWORD:server_ks_password}" + # Password used to access the key + key_password: "${COAP_DTLS_KEY_PASSWORD:server_key_password}" + # Key alias + key_alias: "${COAP_DTLS_KEY_ALIAS:serveralias}" + # Skip certificate validity check for client certificates. + skip_validity_check_for_client_cert: "${COAP_DTLS_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}" + x509: + dtls_session_inactivity_timeout: "${TB_COAP_X509_DTLS_SESSION_INACTIVITY_TIMEOUT:86400000}" + dtls_session_report_timeout: "${TB_COAP_X509_DTLS_SESSION_REPORT_TIMEOUT:1800000}" swagger: api_path_regex: "${SWAGGER_API_PATH_REGEX:/api.*}" security_path_regex: "${SWAGGER_SECURITY_PATH_REGEX:/api.*}" diff --git a/common/transport/coap/pom.xml b/common/transport/coap/pom.xml index b350e5661a..bd2f027e3e 100644 --- a/common/transport/coap/pom.xml +++ b/common/transport/coap/pom.xml @@ -44,6 +44,10 @@ org.eclipse.californium californium-core + + org.eclipse.californium + scandium + org.springframework spring-context-support diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java index bf6c7c994d..83fc56bdc6 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java @@ -46,6 +46,10 @@ public class CoapTransportContext extends TransportContext { @Value("${transport.coap.timeout}") private Long timeout; + @Getter + @Autowired(required = false) + private TbCoapDtlsSettings dtlsSettings; + @Getter @Autowired private JsonCoapAdaptor jsonCoapAdaptor; diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java index c1fd5b6a4a..074be3ae47 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java @@ -27,6 +27,7 @@ import org.eclipse.californium.core.observe.ObserveRelation; import org.eclipse.californium.core.server.resources.CoapExchange; import org.eclipse.californium.core.server.resources.Resource; import org.eclipse.californium.core.server.resources.ResourceObserver; +import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceTransportType; @@ -63,15 +64,22 @@ public class CoapTransportResource extends AbstractCoapTransportResource { private static final int FEATURE_TYPE_POSITION = 4; private static final int REQUEST_ID_POSITION = 5; + private static final int FEATURE_TYPE_POSITION_CERTIFICATE_REQUEST = 3; + private static final int REQUEST_ID_POSITION_CERTIFICATE_REQUEST = 4; + private static final String DTLS_SESSION_ID_KEY = "DTLS_SESSION_ID"; + private final ConcurrentMap tokenToSessionIdMap = new ConcurrentHashMap<>(); private final ConcurrentMap tokenToNotificationCounterMap = new ConcurrentHashMap<>(); private final Set rpcSubscriptions = ConcurrentHashMap.newKeySet(); private final Set attributeSubscriptions = ConcurrentHashMap.newKeySet(); - public CoapTransportResource(CoapTransportContext coapTransportContext, String name) { + private ConcurrentMap dtlsSessionIdMap; + + public CoapTransportResource(CoapTransportContext coapTransportContext, ConcurrentMap dtlsSessionIdMap, String name) { super(coapTransportContext, name); this.setObservable(true); // enable observing this.addObserver(new CoapResourceObserver()); + this.dtlsSessionIdMap = dtlsSessionIdMap; // this.setObservable(false); // disable observing // this.setObserveType(CoAP.Type.CON); // configure the notification type to CONs // this.getAttributes().setObservable(); // mark observable in the Link-Format @@ -187,107 +195,132 @@ public class CoapTransportResource extends AbstractCoapTransportResource { Exchange advanced = exchange.advanced(); Request request = advanced.getRequest(); + String dtlsSessionIdStr = request.getSourceContext().get(DTLS_SESSION_ID_KEY); + if (!StringUtils.isEmpty(dtlsSessionIdStr)) { + if (dtlsSessionIdMap != null) { + TbCoapDtlsSessionInfo tbCoapDtlsSessionInfo = dtlsSessionIdMap + .computeIfPresent(dtlsSessionIdStr, (dtlsSessionId, dtlsSessionInfo) -> { + dtlsSessionInfo.setLastActivityTime(System.currentTimeMillis()); + return dtlsSessionInfo; + }); + if (tbCoapDtlsSessionInfo != null) { + processRequest(exchange, type, request, tbCoapDtlsSessionInfo.getSessionInfoProto(), tbCoapDtlsSessionInfo.getDeviceProfile()); + } else { + exchange.respond(CoAP.ResponseCode.UNAUTHORIZED); + } + } else { + processAccessTokenRequest(exchange, type, request); + } + } else { + processAccessTokenRequest(exchange, type, request); + } + } + + private void processAccessTokenRequest(CoapExchange exchange, SessionMsgType type, Request request) { Optional credentials = decodeCredentials(request); if (credentials.isEmpty()) { - exchange.respond(CoAP.ResponseCode.BAD_REQUEST); + exchange.respond(CoAP.ResponseCode.UNAUTHORIZED); return; } - transportService.process(DeviceTransportType.COAP, TransportProtos.ValidateDeviceTokenRequestMsg.newBuilder().setToken(credentials.get().getCredentialsId()).build(), new CoapDeviceAuthCallback(transportContext, exchange, (sessionInfo, deviceProfile) -> { - UUID sessionId = new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB()); - try { - TransportConfigurationContainer transportConfigurationContainer = getTransportConfigurationContainer(deviceProfile); - CoapTransportAdaptor coapTransportAdaptor = getCoapTransportAdaptor(transportConfigurationContainer.isJsonPayload()); - switch (type) { - case POST_ATTRIBUTES_REQUEST: - transportService.process(sessionInfo, - coapTransportAdaptor.convertToPostAttributes(sessionId, request, - transportConfigurationContainer.getAttributesMsgDescriptor()), - new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); - reportActivity(sessionInfo, attributeSubscriptions.contains(sessionId), rpcSubscriptions.contains(sessionId)); - break; - case POST_TELEMETRY_REQUEST: - transportService.process(sessionInfo, - coapTransportAdaptor.convertToPostTelemetry(sessionId, request, - transportConfigurationContainer.getTelemetryMsgDescriptor()), - new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); - reportActivity(sessionInfo, attributeSubscriptions.contains(sessionId), rpcSubscriptions.contains(sessionId)); - break; - case CLAIM_REQUEST: - transportService.process(sessionInfo, - coapTransportAdaptor.convertToClaimDevice(sessionId, request, sessionInfo), - new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); - break; - case SUBSCRIBE_ATTRIBUTES_REQUEST: - TransportProtos.SessionInfoProto currentAttrSession = tokenToSessionIdMap.get(getTokenFromRequest(request)); - if (currentAttrSession == null) { - attributeSubscriptions.add(sessionId); - registerAsyncCoapSession(exchange, sessionInfo, coapTransportAdaptor, getTokenFromRequest(request)); - transportService.process(sessionInfo, - TransportProtos.SubscribeToAttributeUpdatesMsg.getDefaultInstance(), new CoapNoOpCallback(exchange)); - } - break; - case UNSUBSCRIBE_ATTRIBUTES_REQUEST: - TransportProtos.SessionInfoProto attrSession = lookupAsyncSessionInfo(getTokenFromRequest(request)); - if (attrSession != null) { - UUID attrSessionId = new UUID(attrSession.getSessionIdMSB(), attrSession.getSessionIdLSB()); - attributeSubscriptions.remove(attrSessionId); - transportService.process(attrSession, - TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().setUnsubscribe(true).build(), - new CoapOkCallback(exchange, CoAP.ResponseCode.DELETED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); - closeAndDeregister(sessionInfo, sessionId); - } - break; - case SUBSCRIBE_RPC_COMMANDS_REQUEST: - TransportProtos.SessionInfoProto currentRpcSession = tokenToSessionIdMap.get(getTokenFromRequest(request)); - if (currentRpcSession == null) { - rpcSubscriptions.add(sessionId); - registerAsyncCoapSession(exchange, sessionInfo, coapTransportAdaptor, getTokenFromRequest(request)); - transportService.process(sessionInfo, - TransportProtos.SubscribeToRPCMsg.getDefaultInstance(), - new CoapNoOpCallback(exchange)); - } else { - UUID rpcSessionId = new UUID(currentRpcSession.getSessionIdMSB(), currentRpcSession.getSessionIdLSB()); - reportActivity(currentRpcSession, attributeSubscriptions.contains(rpcSessionId), rpcSubscriptions.contains(rpcSessionId)); - } - break; - case UNSUBSCRIBE_RPC_COMMANDS_REQUEST: - TransportProtos.SessionInfoProto rpcSession = lookupAsyncSessionInfo(getTokenFromRequest(request)); - if (rpcSession != null) { - UUID rpcSessionId = new UUID(rpcSession.getSessionIdMSB(), rpcSession.getSessionIdLSB()); - rpcSubscriptions.remove(rpcSessionId); - transportService.process(rpcSession, - TransportProtos.SubscribeToRPCMsg.newBuilder().setUnsubscribe(true).build(), - new CoapOkCallback(exchange, CoAP.ResponseCode.DELETED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); - closeAndDeregister(sessionInfo, sessionId); - } - break; - case TO_DEVICE_RPC_RESPONSE: - transportService.process(sessionInfo, - coapTransportAdaptor.convertToDeviceRpcResponse(sessionId, request), - new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); - break; - case TO_SERVER_RPC_REQUEST: - transportService.registerSyncSession(sessionInfo, getCoapSessionListener(exchange, coapTransportAdaptor), transportContext.getTimeout()); - transportService.process(sessionInfo, - coapTransportAdaptor.convertToServerRpcRequest(sessionId, request), - new CoapNoOpCallback(exchange)); - break; - case GET_ATTRIBUTES_REQUEST: - transportService.registerSyncSession(sessionInfo, getCoapSessionListener(exchange, coapTransportAdaptor), transportContext.getTimeout()); - transportService.process(sessionInfo, - coapTransportAdaptor.convertToGetAttributes(sessionId, request), - new CoapNoOpCallback(exchange)); - break; - } - } catch (AdaptorException e) { - log.trace("[{}] Failed to decode message: ", sessionId, e); - exchange.respond(CoAP.ResponseCode.BAD_REQUEST); - } + processRequest(exchange, type, request, sessionInfo, deviceProfile); })); } + private void processRequest(CoapExchange exchange, SessionMsgType type, Request request, TransportProtos.SessionInfoProto sessionInfo, DeviceProfile deviceProfile) { + UUID sessionId = new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB()); + try { + TransportConfigurationContainer transportConfigurationContainer = getTransportConfigurationContainer(deviceProfile); + CoapTransportAdaptor coapTransportAdaptor = getCoapTransportAdaptor(transportConfigurationContainer.isJsonPayload()); + switch (type) { + case POST_ATTRIBUTES_REQUEST: + transportService.process(sessionInfo, + coapTransportAdaptor.convertToPostAttributes(sessionId, request, + transportConfigurationContainer.getAttributesMsgDescriptor()), + new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); + reportActivity(sessionInfo, attributeSubscriptions.contains(sessionId), rpcSubscriptions.contains(sessionId)); + break; + case POST_TELEMETRY_REQUEST: + transportService.process(sessionInfo, + coapTransportAdaptor.convertToPostTelemetry(sessionId, request, + transportConfigurationContainer.getTelemetryMsgDescriptor()), + new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); + reportActivity(sessionInfo, attributeSubscriptions.contains(sessionId), rpcSubscriptions.contains(sessionId)); + break; + case CLAIM_REQUEST: + transportService.process(sessionInfo, + coapTransportAdaptor.convertToClaimDevice(sessionId, request, sessionInfo), + new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); + break; + case SUBSCRIBE_ATTRIBUTES_REQUEST: + TransportProtos.SessionInfoProto currentAttrSession = tokenToSessionIdMap.get(getTokenFromRequest(request)); + if (currentAttrSession == null) { + attributeSubscriptions.add(sessionId); + registerAsyncCoapSession(exchange, sessionInfo, coapTransportAdaptor, getTokenFromRequest(request)); + transportService.process(sessionInfo, + TransportProtos.SubscribeToAttributeUpdatesMsg.getDefaultInstance(), new CoapNoOpCallback(exchange)); + } + break; + case UNSUBSCRIBE_ATTRIBUTES_REQUEST: + TransportProtos.SessionInfoProto attrSession = lookupAsyncSessionInfo(getTokenFromRequest(request)); + if (attrSession != null) { + UUID attrSessionId = new UUID(attrSession.getSessionIdMSB(), attrSession.getSessionIdLSB()); + attributeSubscriptions.remove(attrSessionId); + transportService.process(attrSession, + TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().setUnsubscribe(true).build(), + new CoapOkCallback(exchange, CoAP.ResponseCode.DELETED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); + closeAndDeregister(sessionInfo, sessionId); + } + break; + case SUBSCRIBE_RPC_COMMANDS_REQUEST: + TransportProtos.SessionInfoProto currentRpcSession = tokenToSessionIdMap.get(getTokenFromRequest(request)); + if (currentRpcSession == null) { + rpcSubscriptions.add(sessionId); + registerAsyncCoapSession(exchange, sessionInfo, coapTransportAdaptor, getTokenFromRequest(request)); + transportService.process(sessionInfo, + TransportProtos.SubscribeToRPCMsg.getDefaultInstance(), + new CoapNoOpCallback(exchange)); + } else { + UUID rpcSessionId = new UUID(currentRpcSession.getSessionIdMSB(), currentRpcSession.getSessionIdLSB()); + reportActivity(currentRpcSession, attributeSubscriptions.contains(rpcSessionId), rpcSubscriptions.contains(rpcSessionId)); + } + break; + case UNSUBSCRIBE_RPC_COMMANDS_REQUEST: + TransportProtos.SessionInfoProto rpcSession = lookupAsyncSessionInfo(getTokenFromRequest(request)); + if (rpcSession != null) { + UUID rpcSessionId = new UUID(rpcSession.getSessionIdMSB(), rpcSession.getSessionIdLSB()); + rpcSubscriptions.remove(rpcSessionId); + transportService.process(rpcSession, + TransportProtos.SubscribeToRPCMsg.newBuilder().setUnsubscribe(true).build(), + new CoapOkCallback(exchange, CoAP.ResponseCode.DELETED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); + closeAndDeregister(sessionInfo, sessionId); + } + break; + case TO_DEVICE_RPC_RESPONSE: + transportService.process(sessionInfo, + coapTransportAdaptor.convertToDeviceRpcResponse(sessionId, request), + new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); + break; + case TO_SERVER_RPC_REQUEST: + transportService.registerSyncSession(sessionInfo, getCoapSessionListener(exchange, coapTransportAdaptor), transportContext.getTimeout()); + transportService.process(sessionInfo, + coapTransportAdaptor.convertToServerRpcRequest(sessionId, request), + new CoapNoOpCallback(exchange)); + break; + case GET_ATTRIBUTES_REQUEST: + transportService.registerSyncSession(sessionInfo, getCoapSessionListener(exchange, coapTransportAdaptor), transportContext.getTimeout()); + transportService.process(sessionInfo, + coapTransportAdaptor.convertToGetAttributes(sessionId, request), + new CoapNoOpCallback(exchange)); + break; + } + } catch (AdaptorException e) { + log.trace("[{}] Failed to decode message: ", sessionId, e); + exchange.respond(CoAP.ResponseCode.BAD_REQUEST); + } + } + private TransportProtos.SessionInfoProto lookupAsyncSessionInfo(String token) { tokenToNotificationCounterMap.remove(token); return tokenToSessionIdMap.remove(token); @@ -310,7 +343,7 @@ public class CoapTransportResource extends AbstractCoapTransportResource { private Optional decodeCredentials(Request request) { List uriPath = request.getOptions().getUriPath(); - if (uriPath.size() >= ACCESS_TOKEN_POSITION) { + if (uriPath.size() > ACCESS_TOKEN_POSITION) { return Optional.of(new DeviceTokenCredentials(uriPath.get(ACCESS_TOKEN_POSITION - 1))); } else { return Optional.empty(); @@ -322,8 +355,11 @@ public class CoapTransportResource extends AbstractCoapTransportResource { try { if (uriPath.size() >= FEATURE_TYPE_POSITION) { return Optional.of(FeatureType.valueOf(uriPath.get(FEATURE_TYPE_POSITION - 1).toUpperCase())); - } else if (uriPath.size() == 3 && uriPath.contains(DataConstants.PROVISION)) { - return Optional.of(FeatureType.valueOf(DataConstants.PROVISION.toUpperCase())); + } else if (uriPath.size() >= FEATURE_TYPE_POSITION_CERTIFICATE_REQUEST) { + if (uriPath.contains(DataConstants.PROVISION)) { + return Optional.of(FeatureType.valueOf(DataConstants.PROVISION.toUpperCase())); + } + return Optional.of(FeatureType.valueOf(uriPath.get(FEATURE_TYPE_POSITION_CERTIFICATE_REQUEST - 1).toUpperCase())); } } catch (RuntimeException e) { log.warn("Failed to decode feature type: {}", uriPath); @@ -336,6 +372,8 @@ public class CoapTransportResource extends AbstractCoapTransportResource { try { if (uriPath.size() >= REQUEST_ID_POSITION) { return Optional.of(Integer.valueOf(uriPath.get(REQUEST_ID_POSITION - 1))); + } else { + return Optional.of(Integer.valueOf(uriPath.get(REQUEST_ID_POSITION_CERTIFICATE_REQUEST - 1))); } } catch (RuntimeException e) { log.warn("Failed to decode feature type: {}", uriPath); diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java index 452f661a80..a28019c6c0 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java @@ -19,7 +19,10 @@ import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapResource; import org.eclipse.californium.core.CoapServer; import org.eclipse.californium.core.network.CoapEndpoint; +import org.eclipse.californium.core.network.config.NetworkConfig; import org.eclipse.californium.core.server.resources.Resource; +import org.eclipse.californium.scandium.DTLSConnector; +import org.eclipse.californium.scandium.config.DtlsConnectorConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; @@ -30,6 +33,11 @@ import javax.annotation.PreDestroy; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; +import java.util.Random; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; @Service("CoapTransportService") @ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.coap.enabled}'=='true')") @@ -44,34 +52,53 @@ public class CoapTransportService { @Autowired private CoapTransportContext coapTransportContext; + private TbCoapDtlsCertificateVerifier tbDtlsCertificateVerifier; + private CoapServer server; + private ScheduledExecutorService dtlsSessionsExecutor; + @PostConstruct public void init() throws UnknownHostException { log.info("Starting CoAP transport..."); log.info("Starting CoAP transport server"); this.server = new CoapServer(); + + CoapEndpoint.Builder capEndpointBuilder = new CoapEndpoint.Builder(); + + if (isDtlsEnabled()) { + TbCoapDtlsSettings dtlsSettings = coapTransportContext.getDtlsSettings(); + DtlsConnectorConfig dtlsConnectorConfig = dtlsSettings.dtlsConnectorConfig(); + DTLSConnector connector = new DTLSConnector(dtlsConnectorConfig); + capEndpointBuilder.setConnector(connector); + if (dtlsConnectorConfig.isClientAuthenticationRequired()) { + tbDtlsCertificateVerifier = (TbCoapDtlsCertificateVerifier) dtlsConnectorConfig.getAdvancedCertificateVerifier(); + dtlsSessionsExecutor = Executors.newSingleThreadScheduledExecutor(); + dtlsSessionsExecutor.scheduleAtFixedRate(this::evictTimeoutSessions, new Random().nextInt((int) getDtlsSessionReportTimeout()), getDtlsSessionReportTimeout(), TimeUnit.MILLISECONDS); + } + } else { + InetAddress addr = InetAddress.getByName(coapTransportContext.getHost()); + InetSocketAddress sockAddr = new InetSocketAddress(addr, coapTransportContext.getPort()); + capEndpointBuilder.setInetSocketAddress(sockAddr); + capEndpointBuilder.setNetworkConfig(NetworkConfig.getStandard()); + } + CoapEndpoint coapEndpoint = capEndpointBuilder.build(); + + server.addEndpoint(coapEndpoint); + createResources(); Resource root = this.server.getRoot(); TbCoapServerMessageDeliverer messageDeliverer = new TbCoapServerMessageDeliverer(root); this.server.setMessageDeliverer(messageDeliverer); - InetAddress addr = InetAddress.getByName(coapTransportContext.getHost()); - InetSocketAddress sockAddr = new InetSocketAddress(addr, coapTransportContext.getPort()); - - CoapEndpoint.Builder coapEndpoitBuilder = new CoapEndpoint.Builder(); - coapEndpoitBuilder.setInetSocketAddress(sockAddr); - CoapEndpoint coapEndpoint = coapEndpoitBuilder.build(); - - server.addEndpoint(coapEndpoint); server.start(); log.info("CoAP transport started!"); } private void createResources() { CoapResource api = new CoapResource(API); - api.add(new CoapTransportResource(coapTransportContext, V1)); + api.add(new CoapTransportResource(coapTransportContext, getDtlsSessionsMap(), V1)); CoapResource efento = new CoapResource(EFENTO); CoapEfentoTransportResource efentoMeasurementsTransportResource = new CoapEfentoTransportResource(coapTransportContext, MEASUREMENTS); @@ -81,8 +108,27 @@ public class CoapTransportService { server.add(efento); } + private boolean isDtlsEnabled() { + return coapTransportContext.getDtlsSettings() != null; + } + + private ConcurrentMap getDtlsSessionsMap() { + return tbDtlsCertificateVerifier != null ? tbDtlsCertificateVerifier.getTbCoapDtlsSessionIdsMap() : null; + } + + private void evictTimeoutSessions() { + tbDtlsCertificateVerifier.evictTimeoutSessions(); + } + + private long getDtlsSessionReportTimeout() { + return tbDtlsCertificateVerifier.getDtlsSessionReportTimeout(); + } + @PreDestroy public void shutdown() { + if (dtlsSessionsExecutor != null) { + dtlsSessionsExecutor.shutdownNow(); + } log.info("Stopping CoAP transport!"); this.server.destroy(); log.info("CoAP transport stopped!"); diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapDtlsCertificateVerifier.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapDtlsCertificateVerifier.java new file mode 100644 index 0000000000..a94ed6caeb --- /dev/null +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapDtlsCertificateVerifier.java @@ -0,0 +1,161 @@ +/** + * Copyright © 2016-2021 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.transport.coap; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.elements.util.CertPathUtil; +import org.eclipse.californium.scandium.dtls.AlertMessage; +import org.eclipse.californium.scandium.dtls.CertificateMessage; +import org.eclipse.californium.scandium.dtls.CertificateType; +import org.eclipse.californium.scandium.dtls.CertificateVerificationResult; +import org.eclipse.californium.scandium.dtls.ConnectionId; +import org.eclipse.californium.scandium.dtls.DTLSSession; +import org.eclipse.californium.scandium.dtls.HandshakeException; +import org.eclipse.californium.scandium.dtls.HandshakeResultHandler; +import org.eclipse.californium.scandium.dtls.x509.NewAdvancedCertificateVerifier; +import org.eclipse.californium.scandium.util.ServerNames; +import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.msg.EncryptionUtil; +import org.thingsboard.server.common.transport.TransportService; +import org.thingsboard.server.common.transport.TransportServiceCallback; +import org.thingsboard.server.common.transport.auth.SessionInfoCreator; +import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; +import org.thingsboard.server.common.transport.util.SslUtil; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; + +import javax.security.auth.x500.X500Principal; +import java.security.cert.CertPath; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Data +public class TbCoapDtlsCertificateVerifier implements NewAdvancedCertificateVerifier { + + private final TbCoapDtlsSessionInMemoryStorage tbCoapDtlsSessionInMemoryStorage; + + private TransportService transportService; + private TbServiceInfoProvider serviceInfoProvider; + private boolean skipValidityCheckForClientCert; + + public TbCoapDtlsCertificateVerifier(TransportService transportService, TbServiceInfoProvider serviceInfoProvider, long dtlsSessionInactivityTimeout, long dtlsSessionReportTimeout, boolean skipValidityCheckForClientCert) { + this.transportService = transportService; + this.serviceInfoProvider = serviceInfoProvider; + this.skipValidityCheckForClientCert = skipValidityCheckForClientCert; + this.tbCoapDtlsSessionInMemoryStorage = new TbCoapDtlsSessionInMemoryStorage(dtlsSessionInactivityTimeout, dtlsSessionReportTimeout); + } + + @Override + public List getSupportedCertificateType() { + return Collections.singletonList(CertificateType.X_509); + } + + @Override + public CertificateVerificationResult verifyCertificate(ConnectionId cid, ServerNames serverName, Boolean clientUsage, boolean truncateCertificatePath, CertificateMessage message, DTLSSession session) { + try { + String credentialsBody = null; + CertPath certpath = message.getCertificateChain(); + X509Certificate[] chain = certpath.getCertificates().toArray(new X509Certificate[0]); + for (X509Certificate cert : chain) { + try { + if (!skipValidityCheckForClientCert) { + cert.checkValidity(); + } + String strCert = SslUtil.getCertificateString(cert); + String sha3Hash = EncryptionUtil.getSha3Hash(strCert); + final ValidateDeviceCredentialsResponse[] deviceCredentialsResponse = new ValidateDeviceCredentialsResponse[1]; + CountDownLatch latch = new CountDownLatch(1); + transportService.process(DeviceTransportType.COAP, TransportProtos.ValidateDeviceX509CertRequestMsg.newBuilder().setHash(sha3Hash).build(), + new TransportServiceCallback<>() { + @Override + public void onSuccess(ValidateDeviceCredentialsResponse msg) { + if (!StringUtils.isEmpty(msg.getCredentials())) { + deviceCredentialsResponse[0] = msg; + } + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + log.error(e.getMessage(), e); + latch.countDown(); + } + }); + latch.await(10, TimeUnit.SECONDS); + ValidateDeviceCredentialsResponse msg = deviceCredentialsResponse[0]; + if (msg != null && strCert.equals(msg.getCredentials())) { + credentialsBody = msg.getCredentials(); + DeviceProfile deviceProfile = msg.getDeviceProfile(); + if (msg.hasDeviceInfo() && deviceProfile != null) { + TransportProtos.SessionInfoProto sessionInfoProto = SessionInfoCreator.create(msg, serviceInfoProvider.getServiceId(), UUID.randomUUID()); + tbCoapDtlsSessionInMemoryStorage.put(session.getSessionIdentifier().toString(), new TbCoapDtlsSessionInfo(sessionInfoProto, deviceProfile)); + } + break; + } + } catch (InterruptedException | + CertificateEncodingException | + CertificateExpiredException | + CertificateNotYetValidException e) { + log.error(e.getMessage(), e); + } + } + if (credentialsBody == null) { + AlertMessage alert = new AlertMessage(AlertMessage.AlertLevel.FATAL, AlertMessage.AlertDescription.BAD_CERTIFICATE, + session.getPeer()); + throw new HandshakeException("Certificate chain could not be validated", alert); + } else { + return new CertificateVerificationResult(cid, certpath, null); + } + } catch (HandshakeException e) { + log.trace("Certificate validation failed!", e); + return new CertificateVerificationResult(cid, e, null); + } + } + + @Override + public List getAcceptedIssuers() { + return CertPathUtil.toSubjects(null); + } + + @Override + public void setResultHandler(HandshakeResultHandler resultHandler) { + // empty implementation + } + + public ConcurrentMap getTbCoapDtlsSessionIdsMap() { + return tbCoapDtlsSessionInMemoryStorage.getDtlsSessionIdMap(); + } + + public void evictTimeoutSessions() { + tbCoapDtlsSessionInMemoryStorage.evictTimeoutSessions(); + } + + public long getDtlsSessionReportTimeout() { + return tbCoapDtlsSessionInMemoryStorage.getDtlsSessionReportTimeout(); + } +} \ No newline at end of file diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapDtlsSessionInMemoryStorage.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapDtlsSessionInMemoryStorage.java new file mode 100644 index 0000000000..d7dd9c1829 --- /dev/null +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapDtlsSessionInMemoryStorage.java @@ -0,0 +1,55 @@ +/** + * Copyright © 2016-2021 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.transport.coap; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +@Slf4j +@Data +public class TbCoapDtlsSessionInMemoryStorage { + + private final ConcurrentMap dtlsSessionIdMap = new ConcurrentHashMap<>(); + private long dtlsSessionInactivityTimeout; + private long dtlsSessionReportTimeout; + + + public TbCoapDtlsSessionInMemoryStorage(long dtlsSessionInactivityTimeout, long dtlsSessionReportTimeout) { + this.dtlsSessionInactivityTimeout = dtlsSessionInactivityTimeout; + this.dtlsSessionReportTimeout = dtlsSessionReportTimeout; + } + + public void put(String dtlsSessionId, TbCoapDtlsSessionInfo dtlsSessionInfo) { + log.trace("DTLS session added to in-memory store: [{}] timestamp: [{}]", dtlsSessionId, dtlsSessionInfo.getLastActivityTime()); + dtlsSessionIdMap.putIfAbsent(dtlsSessionId, dtlsSessionInfo); + } + + public void evictTimeoutSessions() { + long expTime = System.currentTimeMillis() - dtlsSessionInactivityTimeout; + dtlsSessionIdMap.entrySet().removeIf(entry -> { + if (entry.getValue().getLastActivityTime() < expTime) { + log.trace("DTLS session was removed from in-memory store: [{}]", entry.getKey()); + return true; + } else { + return false; + } + }); + } + +} \ No newline at end of file diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapDtlsSessionInfo.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapDtlsSessionInfo.java new file mode 100644 index 0000000000..452c5eb792 --- /dev/null +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapDtlsSessionInfo.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2021 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.transport.coap; + +import lombok.Data; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.gen.transport.TransportProtos; + +@Data +public class TbCoapDtlsSessionInfo { + + private TransportProtos.SessionInfoProto sessionInfoProto; + private DeviceProfile deviceProfile; + private long lastActivityTime; + + + public TbCoapDtlsSessionInfo(TransportProtos.SessionInfoProto sessionInfoProto, DeviceProfile deviceProfile) { + this.sessionInfoProto = sessionInfoProto; + this.deviceProfile = deviceProfile; + this.lastActivityTime = System.currentTimeMillis(); + } +} \ No newline at end of file diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapDtlsSettings.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapDtlsSettings.java new file mode 100644 index 0000000000..d7cd0b3f39 --- /dev/null +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapDtlsSettings.java @@ -0,0 +1,162 @@ +/** + * Copyright © 2016-2021 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.transport.coap; + +import com.google.common.io.Resources; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.elements.util.SslContextUtil; +import org.eclipse.californium.scandium.config.DtlsConnectorConfig; +import org.eclipse.californium.scandium.dtls.CertificateType; +import org.eclipse.californium.scandium.dtls.x509.StaticNewAdvancedCertificateVerifier; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.transport.TransportService; +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.security.GeneralSecurityException; +import java.security.cert.Certificate; +import java.util.Collections; +import java.util.Optional; + +@Slf4j +@ConditionalOnProperty(prefix = "transport.coap.dtls", value = "enabled", havingValue = "true", matchIfMissing = false) +@ConditionalOnExpression("'${transport.type:null}'=='null' || ('${transport.type}'=='local' && '${transport.coap.enabled}'=='true')") +@Component +public class TbCoapDtlsSettings { + + @Value("${transport.coap.bind_address}") + private String host; + + @Value("${transport.coap.bind_port}") + private Integer port; + + @Value("${transport.coap.dtls.mode}") + private String mode; + + @Value("${transport.coap.dtls.key_store}") + private String keyStoreFile; + + @Value("${transport.coap.dtls.key_store_password}") + private String keyStorePassword; + + @Value("${transport.coap.dtls.key_password}") + private String keyPassword; + + @Value("${transport.coap.dtls.key_alias}") + private String keyAlias; + + @Value("${transport.coap.dtls.skip_validity_check_for_client_cert}") + private boolean skipValidityCheckForClientCert; + + @Value("${transport.coap.dtls.x509.dtls_session_inactivity_timeout}") + private long dtlsSessionInactivityTimeout; + + @Value("${transport.coap.dtls.x509.dtls_session_report_timeout}") + private long dtlsSessionReportTimeout; + + @Autowired + private TransportService transportService; + + @Autowired + private TbServiceInfoProvider serviceInfoProvider; + + public DtlsConnectorConfig dtlsConnectorConfig() throws UnknownHostException { + Optional securityModeOpt = SecurityMode.parse(mode); + if (securityModeOpt.isEmpty()) { + log.warn("Incorrect configuration of securityMode {}", mode); + throw new RuntimeException("Failed to parse mode property: " + mode + "!"); + } else { + DtlsConnectorConfig.Builder configBuilder = new DtlsConnectorConfig.Builder(); + configBuilder.setAddress(getInetSocketAddress()); + String keyStoreFilePath = Resources.getResource(keyStoreFile).getPath(); + SslContextUtil.Credentials serverCredentials = loadServerCredentials(keyStoreFilePath); + SecurityMode securityMode = securityModeOpt.get(); + if (securityMode.equals(SecurityMode.NO_AUTH)) { + configBuilder.setClientAuthenticationRequired(false); + configBuilder.setServerOnly(true); + } else { + configBuilder.setAdvancedCertificateVerifier( + new TbCoapDtlsCertificateVerifier( + transportService, + serviceInfoProvider, + dtlsSessionInactivityTimeout, + dtlsSessionReportTimeout, + skipValidityCheckForClientCert + ) + ); + } + configBuilder.setIdentity(serverCredentials.getPrivateKey(), serverCredentials.getCertificateChain(), + Collections.singletonList(CertificateType.X_509)); + return configBuilder.build(); + } + } + + private SslContextUtil.Credentials loadServerCredentials(String keyStoreFilePath) { + try { + return SslContextUtil.loadCredentials(keyStoreFilePath, keyAlias, keyStorePassword.toCharArray(), + keyPassword.toCharArray()); + } catch (GeneralSecurityException | IOException e) { + throw new RuntimeException("Failed to load serverCredentials due to: ", e); + } + } + + private void loadTrustedCertificates(DtlsConnectorConfig.Builder config, String keyStoreFilePath) { + StaticNewAdvancedCertificateVerifier.Builder trustBuilder = StaticNewAdvancedCertificateVerifier.builder(); + try { + Certificate[] trustedCertificates = SslContextUtil.loadTrustedCertificates( + keyStoreFilePath, keyAlias, + keyStorePassword.toCharArray()); + trustBuilder.setTrustedCertificates(trustedCertificates); + if (trustBuilder.hasTrusts()) { + config.setAdvancedCertificateVerifier(trustBuilder.build()); + } + } catch (GeneralSecurityException | IOException e) { + throw new RuntimeException("Failed to load trusted certificates due to: ", e); + } + } + + private InetSocketAddress getInetSocketAddress() throws UnknownHostException { + InetAddress addr = InetAddress.getByName(host); + return new InetSocketAddress(addr, port); + } + + private enum SecurityMode { + X509, + NO_AUTH; + + static Optional parse(String name) { + SecurityMode mode = null; + if (name != null) { + for (SecurityMode securityMode : SecurityMode.values()) { + if (securityMode.name().equalsIgnoreCase(name)) { + mode = securityMode; + break; + } + } + } + return Optional.ofNullable(mode); + } + + } + +} \ No newline at end of file diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/NoSecClient.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/NoSecClient.java new file mode 100644 index 0000000000..f9a31d0513 --- /dev/null +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/NoSecClient.java @@ -0,0 +1,97 @@ +/** + * Copyright © 2016-2021 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.transport.coap.client; + +import org.eclipse.californium.core.CoapClient; +import org.eclipse.californium.core.CoapResponse; +import org.eclipse.californium.core.Utils; +import org.eclipse.californium.elements.DtlsEndpointContext; +import org.eclipse.californium.elements.EndpointContext; +import org.eclipse.californium.elements.exception.ConnectorException; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.Principal; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class NoSecClient { + + private ExecutorService executor = Executors.newFixedThreadPool(1); + private CoapClient coapClient; + + public NoSecClient(String host, int port, String accessToken, String clientKeys, String sharedKeys) throws URISyntaxException { + URI uri = new URI(getFutureUrl(host, port, accessToken, clientKeys, sharedKeys)); + this.coapClient = new CoapClient(uri); + } + + public void test() { + executor.submit(() -> { + try { + while (!Thread.interrupted()) { + CoapResponse response = null; + try { + response = coapClient.get(); + } catch (ConnectorException | IOException e) { + System.err.println("Error occurred while sending request: " + e); + System.exit(-1); + } + if (response != null) { + + System.out.println(response.getCode() + " - " + response.getCode().name()); + System.out.println(response.getOptions()); + System.out.println(response.getResponseText()); + System.out.println(); + System.out.println("ADVANCED:"); + EndpointContext context = response.advanced().getSourceContext(); + Principal identity = context.getPeerIdentity(); + if (identity != null) { + System.out.println(context.getPeerIdentity()); + } else { + System.out.println("anonymous"); + } + System.out.println(context.get(DtlsEndpointContext.KEY_CIPHER)); + System.out.println(Utils.prettyPrint(response)); + } else { + System.out.println("No response received."); + } + Thread.sleep(5000); + } + } catch (Exception e) { + System.out.println("Error occurred while sending COAP requests."); + } + }); + } + + private String getFutureUrl(String host, Integer port, String accessToken, String clientKeys, String sharedKeys) { + return "coap://" + host + ":" + port + "/api/v1/" + accessToken + "/attributes?clientKeys=" + clientKeys + "&sharedKeys=" + sharedKeys; + } + + public static void main(String[] args) throws URISyntaxException { + System.out.println("Usage: java -cp ... org.thingsboard.server.transport.coap.client.NoSecClient " + + "host port accessToken clientKeys sharedKeys"); + + String host = args[0]; + int port = Integer.parseInt(args[1]); + String accessToken = args[2]; + String clientKeys = args[3]; + String sharedKeys = args[4]; + + NoSecClient client = new NoSecClient(host, port, accessToken, clientKeys, sharedKeys); + client.test(); + } +} diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/SecureClientNoAuth.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/SecureClientNoAuth.java new file mode 100644 index 0000000000..7bbb1f55cf --- /dev/null +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/SecureClientNoAuth.java @@ -0,0 +1,145 @@ +/** + * Copyright © 2016-2021 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.transport.coap.client; + +import org.eclipse.californium.core.CoapClient; +import org.eclipse.californium.core.CoapResponse; +import org.eclipse.californium.core.Utils; +import org.eclipse.californium.core.network.CoapEndpoint; +import org.eclipse.californium.elements.DtlsEndpointContext; +import org.eclipse.californium.elements.EndpointContext; +import org.eclipse.californium.elements.exception.ConnectorException; +import org.eclipse.californium.elements.util.SslContextUtil; +import org.eclipse.californium.scandium.DTLSConnector; +import org.eclipse.californium.scandium.config.DtlsConnectorConfig; +import org.eclipse.californium.scandium.dtls.CertificateType; +import org.eclipse.californium.scandium.dtls.x509.StaticNewAdvancedCertificateVerifier; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.GeneralSecurityException; +import java.security.Principal; +import java.security.cert.Certificate; +import java.util.Collections; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class SecureClientNoAuth { + + private final DTLSConnector dtlsConnector; + private ExecutorService executor = Executors.newFixedThreadPool(1); + private CoapClient coapClient; + + public SecureClientNoAuth(DTLSConnector dtlsConnector, String host, int port, String accessToken, String clientKeys, String sharedKeys) throws URISyntaxException { + this.dtlsConnector = dtlsConnector; + this.coapClient = getCoapClient(host, port, accessToken, clientKeys, sharedKeys); + } + + public void test() { + executor.submit(() -> { + try { + while (!Thread.interrupted()) { + CoapResponse response = null; + try { + response = coapClient.get(); + } catch (ConnectorException | IOException e) { + System.err.println("Error occurred while sending request: " + e); + System.exit(-1); + } + if (response != null) { + + System.out.println(response.getCode() + " - " + response.getCode().name()); + System.out.println(response.getOptions()); + System.out.println(response.getResponseText()); + System.out.println(); + System.out.println("ADVANCED:"); + EndpointContext context = response.advanced().getSourceContext(); + Principal identity = context.getPeerIdentity(); + if (identity != null) { + System.out.println(context.getPeerIdentity()); + } else { + System.out.println("anonymous"); + } + System.out.println(context.get(DtlsEndpointContext.KEY_CIPHER)); + System.out.println(Utils.prettyPrint(response)); + } else { + System.out.println("No response received."); + } + Thread.sleep(5000); + } + } catch (Exception e) { + System.out.println("Error occurred while sending COAP requests."); + } + }); + } + + private CoapClient getCoapClient(String host, Integer port, String accessToken, String clientKeys, String sharedKeys) throws URISyntaxException { + URI uri = new URI(getFutureUrl(host, port, accessToken, clientKeys, sharedKeys)); + CoapClient client = new CoapClient(uri); + CoapEndpoint.Builder builder = new CoapEndpoint.Builder(); + builder.setConnector(dtlsConnector); + + client.setEndpoint(builder.build()); + return client; + } + + private String getFutureUrl(String host, Integer port, String accessToken, String clientKeys, String sharedKeys) { + return "coaps://" + host + ":" + port + "/api/v1/" + accessToken + "/attributes?clientKeys=" + clientKeys + "&sharedKeys=" + sharedKeys; + } + + public static void main(String[] args) throws URISyntaxException { + System.out.println("Usage: java -cp ... org.thingsboard.server.transport.coap.client.SecureClientNoAuth " + + "host port accessToken keyStoreUriPath keyStoreAlias trustedAliasPattern clientKeys sharedKeys"); + + String host = args[0]; + int port = Integer.parseInt(args[1]); + String accessToken = args[2]; + String clientKeys = args[7]; + String sharedKeys = args[8]; + + String keyStoreUriPath = args[3]; + String keyStoreAlias = args[4]; + String trustedAliasPattern = args[5]; + String keyStorePassword = args[6]; + + + DtlsConnectorConfig.Builder builder = new DtlsConnectorConfig.Builder(); + setupCredentials(builder, keyStoreUriPath, keyStoreAlias, trustedAliasPattern, keyStorePassword); + DTLSConnector dtlsConnector = new DTLSConnector(builder.build()); + SecureClientNoAuth client = new SecureClientNoAuth(dtlsConnector, host, port, accessToken, clientKeys, sharedKeys); + client.test(); + } + + private static void setupCredentials(DtlsConnectorConfig.Builder config, String keyStoreUriPath, String keyStoreAlias, String trustedAliasPattern, String keyStorePassword) { + StaticNewAdvancedCertificateVerifier.Builder trustBuilder = StaticNewAdvancedCertificateVerifier.builder(); + try { + SslContextUtil.Credentials serverCredentials = SslContextUtil.loadCredentials( + keyStoreUriPath, keyStoreAlias, keyStorePassword.toCharArray(), keyStorePassword.toCharArray()); + Certificate[] trustedCertificates = SslContextUtil.loadTrustedCertificates( + keyStoreUriPath, trustedAliasPattern, keyStorePassword.toCharArray()); + trustBuilder.setTrustedCertificates(trustedCertificates); + config.setAdvancedCertificateVerifier(trustBuilder.build()); + config.setIdentity(serverCredentials.getPrivateKey(), serverCredentials.getCertificateChain(), Collections.singletonList(CertificateType.X_509)); + } catch (GeneralSecurityException e) { + System.err.println("certificates are invalid!"); + throw new IllegalArgumentException(e.getMessage()); + } catch (IOException e) { + System.err.println("certificates are missing!"); + throw new IllegalArgumentException(e.getMessage()); + } + } +} diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/SecureClientX509.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/SecureClientX509.java new file mode 100644 index 0000000000..31dd628b40 --- /dev/null +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/SecureClientX509.java @@ -0,0 +1,144 @@ +/** + * Copyright © 2016-2021 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.transport.coap.client; + +import org.eclipse.californium.core.CoapClient; +import org.eclipse.californium.core.CoapResponse; +import org.eclipse.californium.core.Utils; +import org.eclipse.californium.core.network.CoapEndpoint; +import org.eclipse.californium.elements.DtlsEndpointContext; +import org.eclipse.californium.elements.EndpointContext; +import org.eclipse.californium.elements.exception.ConnectorException; +import org.eclipse.californium.elements.util.SslContextUtil; +import org.eclipse.californium.scandium.DTLSConnector; +import org.eclipse.californium.scandium.config.DtlsConnectorConfig; +import org.eclipse.californium.scandium.dtls.CertificateType; +import org.eclipse.californium.scandium.dtls.x509.StaticNewAdvancedCertificateVerifier; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.GeneralSecurityException; +import java.security.Principal; +import java.security.cert.Certificate; +import java.util.Collections; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class SecureClientX509 { + + private final DTLSConnector dtlsConnector; + private ExecutorService executor = Executors.newFixedThreadPool(1); + private CoapClient coapClient; + + public SecureClientX509(DTLSConnector dtlsConnector, String host, int port, String clientKeys, String sharedKeys) throws URISyntaxException { + this.dtlsConnector = dtlsConnector; + this.coapClient = getCoapClient(host, port, clientKeys, sharedKeys); + } + + public void test() { + executor.submit(() -> { + try { + while (!Thread.interrupted()) { + CoapResponse response = null; + try { + response = coapClient.get(); + } catch (ConnectorException | IOException e) { + System.err.println("Error occurred while sending request: " + e); + System.exit(-1); + } + if (response != null) { + + System.out.println(response.getCode() + " - " + response.getCode().name()); + System.out.println(response.getOptions()); + System.out.println(response.getResponseText()); + System.out.println(); + System.out.println("ADVANCED:"); + EndpointContext context = response.advanced().getSourceContext(); + Principal identity = context.getPeerIdentity(); + if (identity != null) { + System.out.println(context.getPeerIdentity()); + } else { + System.out.println("anonymous"); + } + System.out.println(context.get(DtlsEndpointContext.KEY_CIPHER)); + System.out.println(Utils.prettyPrint(response)); + } else { + System.out.println("No response received."); + } + Thread.sleep(5000); + } + } catch (Exception e) { + System.out.println("Error occurred while sending COAP requests."); + } + }); + } + + private CoapClient getCoapClient(String host, Integer port, String clientKeys, String sharedKeys) throws URISyntaxException { + URI uri = new URI(getFutureUrl(host, port, clientKeys, sharedKeys)); + CoapClient client = new CoapClient(uri); + CoapEndpoint.Builder builder = new CoapEndpoint.Builder(); + builder.setConnector(dtlsConnector); + + client.setEndpoint(builder.build()); + return client; + } + + private String getFutureUrl(String host, Integer port, String clientKeys, String sharedKeys) { + return "coaps://" + host + ":" + port + "/api/v1/attributes?clientKeys=" + clientKeys + "&sharedKeys=" + sharedKeys; + } + + public static void main(String[] args) throws URISyntaxException { + System.out.println("Usage: java -cp ... org.thingsboard.server.transport.coap.client.SecureClientX509 " + + "host port keyStoreUriPath keyStoreAlias trustedAliasPattern clientKeys sharedKeys"); + + String host = args[0]; + int port = Integer.parseInt(args[1]); + String clientKeys = args[6]; + String sharedKeys = args[7]; + + String keyStoreUriPath = args[2]; + String keyStoreAlias = args[3]; + String trustedAliasPattern = args[4]; + String keyStorePassword = args[5]; + + + DtlsConnectorConfig.Builder builder = new DtlsConnectorConfig.Builder(); + setupCredentials(builder, keyStoreUriPath, keyStoreAlias, trustedAliasPattern, keyStorePassword); + DTLSConnector dtlsConnector = new DTLSConnector(builder.build()); + SecureClientX509 client = new SecureClientX509(dtlsConnector, host, port, clientKeys, sharedKeys); + client.test(); + } + + private static void setupCredentials(DtlsConnectorConfig.Builder config, String keyStoreUriPath, String keyStoreAlias, String trustedAliasPattern, String keyStorePassword) { + StaticNewAdvancedCertificateVerifier.Builder trustBuilder = StaticNewAdvancedCertificateVerifier.builder(); + try { + SslContextUtil.Credentials serverCredentials = SslContextUtil.loadCredentials( + keyStoreUriPath, keyStoreAlias, keyStorePassword.toCharArray(), keyStorePassword.toCharArray()); + Certificate[] trustedCertificates = SslContextUtil.loadTrustedCertificates( + keyStoreUriPath, trustedAliasPattern, keyStorePassword.toCharArray()); + trustBuilder.setTrustedCertificates(trustedCertificates); + config.setAdvancedCertificateVerifier(trustBuilder.build()); + config.setIdentity(serverCredentials.getPrivateKey(), serverCredentials.getCertificateChain(), Collections.singletonList(CertificateType.X_509)); + } catch (GeneralSecurityException e) { + System.err.println("certificates are invalid!"); + throw new IllegalArgumentException(e.getMessage()); + } catch (IOException e) { + System.err.println("certificates are missing!"); + throw new IllegalArgumentException(e.getMessage()); + } + } +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java index c2cf3686e9..1c0801c64a 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java @@ -30,7 +30,7 @@ import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.transport.mqtt.util.SslUtil; +import org.thingsboard.server.common.transport.util.SslUtil; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; @@ -41,7 +41,6 @@ import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; import java.io.File; import java.io.FileInputStream; -import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.security.KeyStore; diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index 06a8dcacdc..6056aa293d 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -66,7 +66,7 @@ import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; import org.thingsboard.server.transport.mqtt.session.DeviceSessionCtx; import org.thingsboard.server.transport.mqtt.session.GatewaySessionHandler; import org.thingsboard.server.transport.mqtt.session.MqttTopicMatcher; -import org.thingsboard.server.transport.mqtt.util.SslUtil; +import org.thingsboard.server.common.transport.util.SslUtil; import javax.net.ssl.SSLPeerUnverifiedException; import java.security.cert.Certificate; diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/SessionInfoCreator.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/SessionInfoCreator.java index ab18b930f9..b175ca8580 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/SessionInfoCreator.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/SessionInfoCreator.java @@ -25,7 +25,15 @@ import java.util.UUID; public class SessionInfoCreator { public static TransportProtos.SessionInfoProto create(ValidateDeviceCredentialsResponse msg, TransportContext context, UUID sessionId) { - return TransportProtos.SessionInfoProto.newBuilder().setNodeId(context.getNodeId()) + return getSessionInfoProto(msg, context.getNodeId(), sessionId); + } + + public static TransportProtos.SessionInfoProto create(ValidateDeviceCredentialsResponse msg, String nodeId, UUID sessionId) { + return getSessionInfoProto(msg, nodeId, sessionId); + } + + private static TransportProtos.SessionInfoProto getSessionInfoProto(ValidateDeviceCredentialsResponse msg, String nodeId, UUID sessionId) { + return TransportProtos.SessionInfoProto.newBuilder().setNodeId(nodeId) .setSessionIdMSB(sessionId.getMostSignificantBits()) .setSessionIdLSB(sessionId.getLeastSignificantBits()) .setDeviceIdMSB(msg.getDeviceInfo().getDeviceId().getId().getMostSignificantBits()) diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/SslUtil.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java similarity index 93% rename from common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/SslUtil.java rename to common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java index f376077b84..77e4045655 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/SslUtil.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java @@ -13,13 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.transport.mqtt.util; +package org.thingsboard.server.common.transport.util; import lombok.extern.slf4j.Slf4j; import org.springframework.util.Base64Utils; import org.thingsboard.server.common.msg.EncryptionUtil; -import java.io.IOException; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; diff --git a/pom.xml b/pom.xml index d1edef19fb..d9af0abf12 100755 --- a/pom.xml +++ b/pom.xml @@ -1152,6 +1152,11 @@ californium-core ${californium.version} + + org.eclipse.californium + scandium + ${californium.version} + com.google.code.gson gson diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index a9fe673b28..0ffcd27f21 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -46,6 +46,24 @@ transport: bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}" bind_port: "${COAP_BIND_PORT:5683}" timeout: "${COAP_TIMEOUT:10000}" + dtls: + # Enable/disable DTLS 1.2 support + enabled: "${COAP_DTLS_ENABLED:false}" + # Secure mode. Allowed values: NO_AUTH, X509 + mode: "${COAP_DTLS_SECURE_MODE:NO_AUTH}" + # Path to the key store that holds the certificate + key_store: "${COAP_DTLS_KEY_STORE:coapserver.jks}" + # Password used to access the key store + key_store_password: "${COAP_DTLS_KEY_STORE_PASSWORD:server_ks_password}" + # Password used to access the key + key_password: "${COAP_DTLS_KEY_PASSWORD:server_key_password}" + # Key alias + key_alias: "${COAP_DTLS_KEY_ALIAS:serveralias}" + # Skip certificate validity check for client certificates. + skip_validity_check_for_client_cert: "${COAP_DTLS_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}" + x509: + dtls_session_inactivity_timeout: "${TB_COAP_X509_DTLS_SESSION_INACTIVITY_TIMEOUT:86400000}" + dtls_session_report_timeout: "${TB_COAP_X509_DTLS_SESSION_REPORT_TIMEOUT:1800000}" sessions: inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}" report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:30000}" diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index 0373f79737..f3ae969911 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -518,7 +518,7 @@ export enum DeviceCredentialsType { export const credentialTypeNames = new Map( [ [DeviceCredentialsType.ACCESS_TOKEN, 'Access token'], - [DeviceCredentialsType.X509_CERTIFICATE, 'MQTT X.509'], + [DeviceCredentialsType.X509_CERTIFICATE, 'X.509'], [DeviceCredentialsType.MQTT_BASIC, 'MQTT Basic'] ] );