fix_bug_coaps_x509_docker_many_devices (#12327)

* coaps: x509 - tests

* coaps: x509 - tests add sever cert.pem

* coaps: x509 - tests add sever cert.pem -2

* coaps: x509 - tests add sever cert.pem -3

* coaps: x509 - tests add sever cert.pem -4

* fix bug: coaps x509 - ddocker many devices with re-port

* fix bug: coaps - add test, connect client with X509

* fix bug: coaps - add two test devices with the same port

* fix bug: coaps - add two test devices with the same port (FeatureType.ATTRIBUTES)

* fix bug: coaps - add two test devices with the same port (FeatureType.ATTRIBUTES) - 1

* fix bug: coap comments 1
This commit is contained in:
Kulikov 2025-01-03 18:17:30 +02:00 committed by GitHub
parent 0a1f377347
commit c5ca395844
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 988 additions and 26 deletions

View File

@ -54,6 +54,14 @@ public abstract class AbstractCoapIntegrationTest extends AbstractTransportInteg
protected final byte[] EMPTY_PAYLOAD = new byte[0];
protected CoapTestClient client;
protected static final String PAYLOAD_VALUES_STR = "{\"key1\":\"value1\", \"key2\":true, \"key3\": 3.0, \"key4\": 4," +
" \"key5\": {\"someNumber\": 42, \"someArray\": [1,2,3], \"someNestedObject\": {\"key\": \"value\"}}}";
protected static final String PAYLOAD_VALUES_STR_01 = "{\"key2\":\"value2\", \"key3\":false, \"key4\": 4.0, \"key5\": 5," +
" \"key6\": {\"someNumber_02\": 52, \"someArray_02\": [1,2,3,4], \"someNestedObject_02\": {\"key_02\": \"value_02\"}}}";
protected void processBeforeTest() throws Exception {
loginTenantAdmin();
}
protected void processAfterTest() throws Exception {
if (client != null) {

View File

@ -63,8 +63,6 @@ import static org.thingsboard.server.common.data.query.EntityKeyType.SHARED_ATTR
@DaoSqlTest
public class CoapClientIntegrationTest extends AbstractCoapIntegrationTest {
private static final String PAYLOAD_VALUES_STR = "{\"key1\":\"value1\", \"key2\":true, \"key3\": 3.0, \"key4\": 4," +
" \"key5\": {\"someNumber\": 42, \"someArray\": [1,2,3], \"someNestedObject\": {\"key\": \"value\"}}}";
private static final List<String> EXPECTED_KEYS = Arrays.asList("key1", "key2", "key3", "key4", "key5");
private static final String DEVICE_RESPONSE = "{\"value1\":\"A\",\"value2\":\"B\"}";

View File

@ -0,0 +1,291 @@
/**
* Copyright © 2016-2024 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.security;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapResponse;
import org.eclipse.californium.core.coap.CoAP;
import org.junit.Assert;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.CoapDeviceType;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.TransportPayloadType;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.common.msg.session.FeatureType;
import org.thingsboard.server.transport.coap.AbstractCoapIntegrationTest;
import org.thingsboard.server.transport.coap.x509.CertPrivateKey;
import org.thingsboard.server.transport.coap.x509.CoapClientX509Test;
import org.thingsboard.server.transport.coap.CoapTestConfigProperties;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import static org.awaitility.Awaitility.await;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Slf4j
@TestPropertySource(properties = {
"coap.enabled=true",
"coap.dtls.enabled=true",
"coap.dtls.credentials.pem.cert_file=coap/credentials/server/cert.pem",
"device.connectivity.coaps.enabled=true",
"service.integrations.supported=ALL",
"transport.coap.enabled=true",
})
public abstract class AbstractCoapSecurityIntegrationTest extends AbstractCoapIntegrationTest {
private static final String COAPS_BASE_URL = "coaps://localhost:5684/api/v1/";
protected final String CREDENTIALS_PATH = "coap/credentials/";
protected final String CREDENTIALS_PATH_CLIENT = CREDENTIALS_PATH + "client/";
protected final String CREDENTIALS_PATH_CLIENT_CERT_PEM = CREDENTIALS_PATH_CLIENT + "cert.pem";
protected final String CREDENTIALS_PATH_CLIENT_KEY_PEM = CREDENTIALS_PATH_CLIENT + "key.pem";
protected final X509Certificate clientX509CertTrustNo; // client certificate signed by intermediate, rootCA with a good CN ("host name")
protected final PrivateKey clientPrivateKeyFromCertTrustNo;
protected static final String CLIENT_JKS_FOR_TEST = "coapclientTest";
protected static final String CLIENT_STORE_PWD = "client_ks_password";
protected static final String CLIENT_ALIAS_CERT_TRUST_NO = "client_alias_trust_no";
protected AbstractCoapSecurityIntegrationTest() {
try {
// Get certificates from key store
char[] clientKeyStorePwd = CLIENT_STORE_PWD.toCharArray();
KeyStore clientKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
try (InputStream clientKeyStoreFile =
this.getClass().getClassLoader().
getResourceAsStream(CREDENTIALS_PATH + CLIENT_JKS_FOR_TEST + ".jks")) {
clientKeyStore.load(clientKeyStoreFile, clientKeyStorePwd);
}
// No trust
clientPrivateKeyFromCertTrustNo = (PrivateKey) clientKeyStore.getKey(CLIENT_ALIAS_CERT_TRUST_NO, clientKeyStorePwd);
clientX509CertTrustNo = (X509Certificate) clientKeyStore.getCertificate(CLIENT_ALIAS_CERT_TRUST_NO);
} catch (GeneralSecurityException | IOException e) {
throw new RuntimeException(e);
}
}
protected Device createDeviceWithX509(String deviceName, DeviceProfileId deviceProfileId, X509Certificate clientX509Cert) throws Exception {
Device device = new Device();
device.setName(deviceName);
device.setType(deviceName);
device.setDeviceProfileId(deviceProfileId);
DeviceCredentials deviceCredentials = new DeviceCredentials();
deviceCredentials.setCredentialsType(DeviceCredentialsType.X509_CERTIFICATE);
String pemFormatCert = CertPrivateKey.convertCertToPEM(clientX509Cert);
deviceCredentials.setCredentialsValue(pemFormatCert);
SaveDeviceWithCredentialsRequest saveRequest = new SaveDeviceWithCredentialsRequest(device, deviceCredentials);
Device deviceX509 = readResponse(doPost("/api/device-with-credentials", saveRequest)
.andExpect(status().isOk()), Device.class);
DeviceCredentials savedDeviceCredentials =
doGet("/api/device/" + deviceX509.getId().getId() + "/credentials", DeviceCredentials.class);
Assert.assertNotNull(savedDeviceCredentials);
Assert.assertNotNull(savedDeviceCredentials.getId());
Assert.assertEquals(deviceX509.getId(), savedDeviceCredentials.getDeviceId());
Assert.assertEquals(DeviceCredentialsType.X509_CERTIFICATE, savedDeviceCredentials.getCredentialsType());
accessToken = savedDeviceCredentials.getCredentialsId();
assertNotNull(accessToken);
return deviceX509;
}
protected void clientX509FromJksUpdateAttributesTest() throws Exception {
CertPrivateKey certPrivateKey = new CertPrivateKey(clientX509CertTrustNo, clientPrivateKeyFromCertTrustNo);
CoapTestConfigProperties configProperties = CoapTestConfigProperties.builder()
.coapDeviceType(CoapDeviceType.DEFAULT)
.transportPayloadType(TransportPayloadType.JSON)
.build();
DeviceProfile deviceProfile = createCoapDeviceProfile(configProperties);
assertNotNull(deviceProfile);
CoapClientX509Test clientX509 = clientX509UpdateTest(FeatureType.ATTRIBUTES, certPrivateKey,
"CoapX509TrustNo_" + FeatureType.ATTRIBUTES.name(), deviceProfile.getId(), null);
clientX509.disconnect();
}
protected void clientX509FromPathUpdateFeatureTypeTest(FeatureType featureType) throws Exception {
CertPrivateKey certPrivateKey = new CertPrivateKey(CREDENTIALS_PATH_CLIENT_CERT_PEM, CREDENTIALS_PATH_CLIENT_KEY_PEM);
CoapTestConfigProperties configProperties = CoapTestConfigProperties.builder()
.coapDeviceType(CoapDeviceType.DEFAULT)
.transportPayloadType(TransportPayloadType.JSON)
.build();
DeviceProfile deviceProfile = createCoapDeviceProfile(configProperties);
assertNotNull(deviceProfile);
CoapClientX509Test clientX509 = clientX509UpdateTest(featureType, certPrivateKey,
"CoapX509TrustNo_" + featureType.name(), deviceProfile.getId(), null);
clientX509.disconnect();
}
protected void twoClientWithSamePortX509FromPathConnectTest() throws Exception {
CoapTestConfigProperties configProperties = CoapTestConfigProperties.builder()
.coapDeviceType(CoapDeviceType.DEFAULT)
.transportPayloadType(TransportPayloadType.JSON)
.build();
DeviceProfile deviceProfile = createCoapDeviceProfile(configProperties);
CertPrivateKey certPrivateKey = new CertPrivateKey(CREDENTIALS_PATH_CLIENT_CERT_PEM, CREDENTIALS_PATH_CLIENT_KEY_PEM);
CertPrivateKey certPrivateKey_01 = new CertPrivateKey(CREDENTIALS_PATH_CLIENT + "cert_01.pem",
CREDENTIALS_PATH_CLIENT + "key_01.pem");
Integer fixedPort = getFreePort();
CoapClientX509Test clientX509 = clientX509UpdateTest(FeatureType.ATTRIBUTES, certPrivateKey,
"CoapX509TrustNo_" + FeatureType.TELEMETRY.name(), deviceProfile.getId(), fixedPort);
clientX509.disconnect();
await("Need to make port " + fixedPort + " free")
.atMost(40, TimeUnit.SECONDS)
.until(() -> isPortAvailable(fixedPort));
CoapClientX509Test clientX509_01 = clientX509UpdateTest(FeatureType.ATTRIBUTES, certPrivateKey_01,
"CoapX509TrustNo_" + FeatureType.TELEMETRY.name() + "_01", deviceProfile.getId(),
fixedPort, PAYLOAD_VALUES_STR_01);
clientX509_01.disconnect();
}
private CoapClientX509Test clientX509UpdateTest(FeatureType featureType, CertPrivateKey certPrivateKey,
String deviceName, DeviceProfileId deviceProfileId, Integer fixedPort) throws Exception {
return clientX509UpdateTest(featureType, certPrivateKey, deviceName, deviceProfileId, fixedPort, null);
}
private CoapClientX509Test clientX509UpdateTest(FeatureType featureType, CertPrivateKey certPrivateKey,
String deviceName, DeviceProfileId deviceProfileId, Integer fixedPort, String payload) throws Exception {
String payloadValuesStr = payload == null ? PAYLOAD_VALUES_STR : payload;
Device deviceX509 = createDeviceWithX509(deviceName, deviceProfileId, certPrivateKey.getCert());
CoapClientX509Test clientX509 = new CoapClientX509Test(certPrivateKey, featureType, COAPS_BASE_URL, fixedPort);
CoapResponse coapResponseX509 = clientX509.postMethod(payloadValuesStr);
assertNotNull(coapResponseX509);
assertEquals(CoAP.ResponseCode.CREATED, coapResponseX509.getCode());
if (FeatureType.ATTRIBUTES.equals(featureType)) {
DeviceId deviceId = deviceX509.getId();
JsonNode expectedNode = JacksonUtil.toJsonNode(payloadValuesStr);
List<String> expectedKeys = getKeysFromNode(expectedNode);
List<String> actualKeys = getActualKeysList(deviceId, expectedKeys, "attributes/CLIENT_SCOPE");
assertNotNull(actualKeys);
Set<String> actualKeySet = new HashSet<>(actualKeys);
Set<String> expectedKeySet = new HashSet<>(expectedKeys);
assertEquals(expectedKeySet, actualKeySet);
String getAttributesValuesUrl = getAttributesValuesUrl(deviceId, actualKeySet, "attributes/CLIENT_SCOPE");
List<Map<String, Object>> actualValues = doGetAsyncTyped(getAttributesValuesUrl, new TypeReference<>() {
});
assertValuesList(actualValues, expectedNode);
}
return clientX509;
}
private List<String> getActualKeysList(DeviceId deviceId, List<String> expectedKeys, String apiSuffix) throws Exception {
long start = System.currentTimeMillis();
long end = System.currentTimeMillis() + 5000;
List<String> actualKeys = null;
while (start <= end) {
actualKeys = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + deviceId + "/keys/" + apiSuffix, new TypeReference<>() {
});
if (actualKeys.size() == expectedKeys.size()) {
break;
}
Thread.sleep(100);
start += 100;
}
return actualKeys;
}
private String getAttributesValuesUrl(DeviceId deviceId, Set<String> actualKeySet, String apiSuffix) {
return "/api/plugins/telemetry/DEVICE/" + deviceId + "/values/" + apiSuffix + "?keys=" + String.join(",", actualKeySet);
}
private List getKeysFromNode(JsonNode jNode) {
List<String> jKeys = new ArrayList<>();
Iterator<String> fieldNames = jNode.fieldNames();
while (fieldNames.hasNext()) {
jKeys.add(fieldNames.next());
}
return jKeys;
}
protected void assertValuesList(List<Map<String, Object>> actualValues, JsonNode expectedValues) {
assertTrue(actualValues.size() > 0);
assertEquals(expectedValues.size(), actualValues.size());
for (Map<String, Object> map : actualValues) {
String key = (String) map.get("key");
Object actualValue = map.get("value");
assertTrue(expectedValues.has(key));
JsonNode expectedValue = expectedValues.get(key);
assertExpectedActualValue(expectedValue, actualValue);
}
}
protected void assertExpectedActualValue(JsonNode expectedValue, Object actualValue) {
switch (expectedValue.getNodeType()) {
case STRING:
assertEquals(expectedValue.asText(), actualValue);
break;
case NUMBER:
if (expectedValue.isInt()) {
assertEquals(expectedValue.asInt(), actualValue);
} else if (expectedValue.isLong()) {
assertEquals(expectedValue.asLong(), actualValue);
} else if (expectedValue.isFloat() || expectedValue.isDouble()) {
assertEquals(expectedValue.asDouble(), actualValue);
}
break;
case BOOLEAN:
assertEquals(expectedValue.asBoolean(), actualValue);
break;
case ARRAY:
case OBJECT:
expectedValue.toString().equals(JacksonUtil.toString(actualValue));
break;
default:
break;
}
}
private static int getFreePort() throws IOException {
try (ServerSocket socket = new ServerSocket(0)) {
return socket.getLocalPort();
}
}
private static boolean isPortAvailable(int port) {
try (ServerSocket serverSocket = new ServerSocket(port)) {
serverSocket.setReuseAddress(true);
return true;
} catch (IOException e) {
return false;
}
}
}

View File

@ -0,0 +1,44 @@
/**
* Copyright © 2016-2024 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.security.sql;
import lombok.extern.slf4j.Slf4j;
import org.junit.Before;
import org.junit.Test;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.transport.coap.security.AbstractCoapSecurityIntegrationTest;
@Slf4j
@DaoSqlTest
@TestPropertySource(properties = {
"coap.dtls.credentials.type=KEYSTORE",
"coap.dtls.credentials.keystore.store_file=coap/credentials/coapserverTest.jks",
"coap.dtls.credentials.keystore.key_password=server_ks_password",
"coap.dtls.credentials.keystore.key_alias=server",
})
public class CoapClientX509SecurityJksIntegrationTest extends AbstractCoapSecurityIntegrationTest {
@Before
public void beforeTest() throws Exception {
processBeforeTest();
}
@Test
public void testX509NoTrustFromJksConnectCoapSuccessUpdateAttributesSuccess() throws Exception {
clientX509FromJksUpdateAttributesTest();
}
}

View File

@ -0,0 +1,44 @@
/**
* Copyright © 2016-2024 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.security.sql;
import lombok.extern.slf4j.Slf4j;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.server.common.msg.session.FeatureType;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.transport.coap.security.AbstractCoapSecurityIntegrationTest;
@Slf4j
@DaoSqlTest
public class CoapClientX509SecurityPemIntegrationTest extends AbstractCoapSecurityIntegrationTest {
@Before
public void beforeTest() throws Exception {
processBeforeTest();
}
@Test
public void testX509NoTrustFromPathConnectCoapSuccessUpdateAttributesSuccess() throws Exception {
clientX509FromPathUpdateFeatureTypeTest(FeatureType.ATTRIBUTES);
}
@Test
public void testX509NoTrustFromPathConnectCoapSuccessUpdateTelemetrySuccess() throws Exception {
clientX509FromPathUpdateFeatureTypeTest(FeatureType.TELEMETRY);
} @Test
public void testTwoDevicesWithSamePortX509NoTrustFromPathConnectCoapSuccess() throws Exception {
twoClientWithSamePortX509FromPathConnectTest();
}
}

View File

@ -44,9 +44,6 @@ import static org.junit.Assert.assertTrue;
@DaoSqlTest
public class CoapAttributesIntegrationTest extends AbstractCoapIntegrationTest {
private static final String PAYLOAD_VALUES_STR = "{\"key1\":\"value1\", \"key2\":true, \"key3\": 3.0, \"key4\": 4," +
" \"key5\": {\"someNumber\": 42, \"someArray\": [1,2,3], \"someNestedObject\": {\"key\": \"value\"}}}";
@Before
public void beforeTest() throws Exception {
CoapTestConfigProperties configProperties = CoapTestConfigProperties.builder()

View File

@ -40,9 +40,6 @@ import static org.junit.Assert.assertNotNull;
@Slf4j
public abstract class AbstractCoapTimeseriesIntegrationTest extends AbstractCoapIntegrationTest {
private static final String PAYLOAD_VALUES_STR = "{\"key1\":\"value1\", \"key2\":true, \"key3\": 3.0, \"key4\": 4," +
" \"key5\": {\"someNumber\": 42, \"someArray\": [1,2,3], \"someNestedObject\": {\"key\": \"value\"}}}";
@Before
public void beforeTest() throws Exception {
CoapTestConfigProperties configProperties = CoapTestConfigProperties.builder()

View File

@ -0,0 +1,82 @@
/**
* Copyright © 2016-2024 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.x509;
import org.apache.commons.io.FileUtils;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
import org.thingsboard.common.util.SslUtil;
import java.io.File;
import java.io.IOException;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.security.interfaces.ECPrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.List;
public class CertPrivateKey {
private final X509Certificate cert;
private PrivateKey privateKey;
public CertPrivateKey(String certFilePathPem, String keyFilePathPem) throws Exception {
List<X509Certificate> certs = SslUtil.readCertFile(fileRead(certFilePathPem));
this.cert = certs.get(0);
this.privateKey = SslUtil.readPrivateKey(fileRead(keyFilePathPem), null);
if (this.privateKey instanceof BCECPrivateKey) {
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(this.privateKey.getEncoded());
KeyFactory keyFactory = KeyFactory.getInstance("EC");
this.privateKey = keyFactory.generatePrivate(keySpec);
}
if (!(this.privateKey instanceof ECPrivateKey)) {
throw new RuntimeException("Private key generation must be of type java.security.interfaces.ECPrivateKey, which is used in the standard Java API!");
}
}
public CertPrivateKey(X509Certificate cert, PrivateKey privateKey) {
this.cert = cert;
this.privateKey = privateKey;
}
public X509Certificate getCert() {
return this.cert;
}
public PrivateKey getPrivateKey() {
return this.privateKey;
}
private String fileRead(String fileName) throws IOException {
ClassLoader classLoader = getClass().getClassLoader();
File file = new File(classLoader.getResource(fileName).getFile());
return FileUtils.readFileToString(file, "UTF-8");
}
public static String convertCertToPEM(X509Certificate certificate) throws Exception {
StringBuilder pemBuilder = new StringBuilder();
pemBuilder.append("-----BEGIN CERTIFICATE-----\n");
// Copy cert to Base64
String base64EncodedCert = Base64.getEncoder().encodeToString(certificate.getEncoded());
int index = 0;
while (index < base64EncodedCert.length()) {
pemBuilder.append(base64EncodedCert, index, Math.min(index + 64, base64EncodedCert.length()));
pemBuilder.append("\n");
index += 64;
}
pemBuilder.append("-----END CERTIFICATE-----\n");
return pemBuilder.toString();
}
}

View File

@ -0,0 +1,239 @@
/**
* Copyright © 2016-2024 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.x509;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapClient;
import org.eclipse.californium.core.CoapHandler;
import org.eclipse.californium.core.CoapObserveRelation;
import org.eclipse.californium.core.CoapResponse;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.coap.MediaTypeRegistry;
import org.eclipse.californium.core.coap.Request;
import org.eclipse.californium.core.config.CoapConfig;
import org.eclipse.californium.core.network.CoapEndpoint;
import org.eclipse.californium.elements.config.Configuration;
import org.eclipse.californium.elements.config.ValueException;
import org.eclipse.californium.elements.exception.ConnectorException;
import org.eclipse.californium.scandium.DTLSConnector;
import org.eclipse.californium.scandium.config.DtlsConfig.SignatureAndHashAlgorithmsDefinition;
import org.eclipse.californium.scandium.config.DtlsConnectorConfig;
import org.eclipse.californium.scandium.dtls.CertificateType;
import org.eclipse.californium.scandium.dtls.SignatureAndHashAlgorithm;
import org.eclipse.californium.scandium.dtls.SignatureAndHashAlgorithm.HashAlgorithm;
import org.eclipse.californium.scandium.dtls.SignatureAndHashAlgorithm.SignatureAlgorithm;
import org.eclipse.californium.scandium.dtls.cipher.CipherSuite;
import org.eclipse.californium.scandium.dtls.x509.CertificateProvider;
import org.eclipse.californium.scandium.dtls.x509.SingleCertificateProvider;
import org.thingsboard.server.common.msg.session.FeatureType;
import org.thingsboard.server.transport.coap.CoapTestCallback;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.eclipse.californium.core.config.CoapConfig.DEFAULT_BLOCKWISE_STATUS_LIFETIME_IN_SECONDS;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_AUTO_HANDSHAKE_TIMEOUT;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CIPHER_SUITES;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_MAX_FRAGMENTED_HANDSHAKE_MESSAGE_LENGTH;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_MAX_PENDING_HANDSHAKE_RESULT_JOBS;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_MAX_RETRANSMISSIONS;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_MAX_RETRANSMISSION_TIMEOUT;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECEIVE_BUFFER_SIZE;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RETRANSMISSION_TIMEOUT;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_ROLE;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_SIGNATURE_AND_HASH_ALGORITHMS;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_USE_HELLO_VERIFY_REQUEST;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_USE_MULTI_HANDSHAKE_MESSAGE_RECORDS;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_VERIFY_SERVER_CERTIFICATES_SUBJECT;
import static org.eclipse.californium.scandium.config.DtlsConfig.DtlsRole.CLIENT_ONLY;
import static org.eclipse.californium.scandium.config.DtlsConfig.MODULE;
import static org.eclipse.californium.scandium.dtls.SignatureAndHashAlgorithm.SHA256_WITH_ECDSA;
import static org.eclipse.californium.scandium.dtls.SignatureAndHashAlgorithm.SHA256_WITH_RSA;
import static org.eclipse.californium.scandium.dtls.SignatureAndHashAlgorithm.SHA384_WITH_ECDSA;
@Slf4j
public class CoapClientX509Test {
private static final long CLIENT_REQUEST_TIMEOUT = 60000L;
private final CoapClient clientX509;
private final DTLSConnector dtlsConnector;
private final Configuration config;
private final CertPrivateKey certPrivateKey;
private final String coapsBaseUrl;
@Getter
private CoAP.Type type = CoAP.Type.CON;
public CoapClientX509Test(CertPrivateKey certPrivateKey, FeatureType featureType, String coapsBaseUrl, Integer fixedPort) {
this.certPrivateKey = certPrivateKey;
this.coapsBaseUrl = coapsBaseUrl;
this.config = createConfiguration();
this.dtlsConnector = createDTLSConnector(fixedPort);
this.clientX509 = createClient(getFeatureTokenUrl(featureType));
}
public void disconnect() {
if (clientX509 != null) {
clientX509.shutdown();
}
}
public CoapResponse postMethod(String requestBody) throws ConnectorException, IOException {
return this.postMethod(requestBody.getBytes());
}
public CoapResponse postMethod(byte[] requestBodyBytes) throws ConnectorException, IOException {
return clientX509.setTimeout(CLIENT_REQUEST_TIMEOUT).post(requestBodyBytes, MediaTypeRegistry.APPLICATION_JSON);
}
public void postMethod(CoapHandler handler, String payload, int format) {
clientX509.setTimeout(CLIENT_REQUEST_TIMEOUT).post(handler, payload, format);
}
public void postMethod(CoapHandler handler, byte[] payload) {
clientX509.setTimeout(CLIENT_REQUEST_TIMEOUT).post(handler, payload, MediaTypeRegistry.APPLICATION_JSON);
}
public void postMethod(CoapHandler handler, byte[] payload, int format) {
clientX509.setTimeout(CLIENT_REQUEST_TIMEOUT).post(handler, payload, format);
}
public CoapResponse getMethod() throws ConnectorException, IOException {
return clientX509.setTimeout(CLIENT_REQUEST_TIMEOUT).get();
}
public CoapObserveRelation getObserveRelation(CoapTestCallback callback) {
return getObserveRelation(callback, true);
}
public CoapObserveRelation getObserveRelation(CoapTestCallback callback, boolean confirmable) {
Request request = Request.newGet().setObserve();
request.setType(confirmable ? CoAP.Type.CON : CoAP.Type.NON);
return clientX509.observe(request, callback);
}
public void setURI(String featureTokenUrl) {
if (clientX509 == null) {
throw new RuntimeException("Failed to connect! CoapClient is not initialized!");
}
clientX509.setURI(featureTokenUrl);
}
public void setURI(String accessToken, FeatureType featureType) {
if (featureType == null) {
featureType = FeatureType.ATTRIBUTES;
}
setURI(getFeatureTokenUrl(accessToken, featureType));
}
public void useCONs() {
if (clientX509 == null) {
throw new RuntimeException("Failed to connect! CoapClient is not initialized!");
}
type = CoAP.Type.CON;
clientX509.useCONs();
}
public void useNONs() {
if (clientX509 == null) {
throw new RuntimeException("Failed to connect! CoapClient is not initialized!");
}
type = CoAP.Type.NON;
clientX509.useNONs();
}
private Configuration createConfiguration() {
Configuration clientCoapConfig = new Configuration();
clientCoapConfig.set(CoapConfig.BLOCKWISE_STRICT_BLOCK2_OPTION, true);
clientCoapConfig.set(CoapConfig.BLOCKWISE_ENTITY_TOO_LARGE_AUTO_FAILOVER, true);
clientCoapConfig.set(CoapConfig.BLOCKWISE_STATUS_LIFETIME, DEFAULT_BLOCKWISE_STATUS_LIFETIME_IN_SECONDS, TimeUnit.SECONDS);
clientCoapConfig.set(CoapConfig.MAX_RESOURCE_BODY_SIZE, 256 * 1024 * 1024);
clientCoapConfig.set(CoapConfig.RESPONSE_MATCHING, CoapConfig.MatcherMode.RELAXED);
clientCoapConfig.set(CoapConfig.PREFERRED_BLOCK_SIZE, 1024);
clientCoapConfig.set(CoapConfig.MAX_MESSAGE_SIZE, 1024);
clientCoapConfig.set(DTLS_ROLE, CLIENT_ONLY);
clientCoapConfig.set(DTLS_MAX_RETRANSMISSIONS, 2);
clientCoapConfig.set(DTLS_RETRANSMISSION_TIMEOUT, 5000, MILLISECONDS);
clientCoapConfig.set(DTLS_MAX_RETRANSMISSION_TIMEOUT, 60000, TimeUnit.MILLISECONDS);
clientCoapConfig.set(DTLS_USE_HELLO_VERIFY_REQUEST, false);
clientCoapConfig.set(DTLS_VERIFY_SERVER_CERTIFICATES_SUBJECT, false);
clientCoapConfig.set(DTLS_MAX_FRAGMENTED_HANDSHAKE_MESSAGE_LENGTH, 22490);
clientCoapConfig.set(DTLS_AUTO_HANDSHAKE_TIMEOUT, 100000, TimeUnit.MILLISECONDS);
clientCoapConfig.set(DTLS_MAX_PENDING_HANDSHAKE_RESULT_JOBS, 64);
clientCoapConfig.set(DTLS_USE_MULTI_HANDSHAKE_MESSAGE_RECORDS, false);
clientCoapConfig.set(DTLS_RECEIVE_BUFFER_SIZE, 8192);
clientCoapConfig.setTransient(DTLS_RECOMMENDED_CIPHER_SUITES_ONLY);
SignatureAndHashAlgorithmsDefinition algorithmsDefinition = new SignatureAndHashAlgorithmsDefinition(MODULE + "SIGNATURE_AND_HASH_ALGORITHMS", "List of DTLS signature- and hash-algorithms.\nValues e.g SHA256withECDSA or ED25519.");
SignatureAndHashAlgorithm SHA384_WITH_RSA = new SignatureAndHashAlgorithm(HashAlgorithm.SHA384,
SignatureAlgorithm.RSA);
List<SignatureAndHashAlgorithm> algorithms = null;
try {
algorithms = algorithmsDefinition.checkValue(Arrays.asList(SHA256_WITH_ECDSA, SHA256_WITH_RSA, SHA384_WITH_ECDSA, SHA384_WITH_RSA));
} catch (ValueException e) {
throw new RuntimeException(e);
}
clientCoapConfig.setTransient(DTLS_SIGNATURE_AND_HASH_ALGORITHMS);
clientCoapConfig.set(DTLS_SIGNATURE_AND_HASH_ALGORITHMS, algorithms);
clientCoapConfig.setTransient(DTLS_CIPHER_SUITES);
clientCoapConfig.set(DTLS_CIPHER_SUITES, Arrays.asList(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256));
clientCoapConfig.setTransient(DTLS_VERIFY_SERVER_CERTIFICATES_SUBJECT);
clientCoapConfig.set(DTLS_VERIFY_SERVER_CERTIFICATES_SUBJECT, false);
return clientCoapConfig;
}
private DTLSConnector createDTLSConnector(Integer fixedPort) {
try {
// Create DTLS config client
DtlsConnectorConfig.Builder configBuilder = new DtlsConnectorConfig.Builder(this.config);
configBuilder.setAdvancedCertificateVerifier(new TbAdvancedCertificateVerifier());
X509Certificate[] certificateChainClient = new X509Certificate[]{this.certPrivateKey.getCert()};
CertificateProvider certificateProvider = new SingleCertificateProvider(this.certPrivateKey.getPrivateKey(), certificateChainClient, Collections.singletonList(CertificateType.X_509));
configBuilder.setCertificateIdentityProvider(certificateProvider);
if (fixedPort != null) {
InetSocketAddress localAddress = new InetSocketAddress("0.0.0.0", fixedPort);
configBuilder.setAddress(localAddress);
configBuilder.setReuseAddress(true);
}
return new DTLSConnector(configBuilder.build());
} catch (Exception e) {
throw new RuntimeException("", e);
}
}
private CoapClient createClient(String featureTokenUrl) {
CoapClient client = new CoapClient(featureTokenUrl);
CoapEndpoint.Builder builder = new CoapEndpoint.Builder();
builder.setConnector(dtlsConnector);
client.setEndpoint(builder.build());
return client;
}
public String getFeatureTokenUrl(FeatureType featureType) {
return this.coapsBaseUrl + featureType.name().toLowerCase();
}
public String getFeatureTokenUrl(String token, FeatureType featureType) {
return this.coapsBaseUrl + token + "/" + featureType.name().toLowerCase();
}
}

View File

@ -0,0 +1,129 @@
/**
* Copyright © 2016-2024 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.x509;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.scandium.dtls.AlertMessage;
import org.eclipse.californium.scandium.dtls.AlertMessage.AlertDescription;
import org.eclipse.californium.scandium.dtls.AlertMessage.AlertLevel;
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.HandshakeException;
import org.eclipse.californium.scandium.dtls.HandshakeResultHandler;
import org.eclipse.californium.scandium.dtls.x509.NewAdvancedCertificateVerifier;
import org.eclipse.californium.scandium.util.ServerNames;
import javax.security.auth.x500.X500Principal;
import java.net.InetSocketAddress;
import java.security.PublicKey;
import java.security.cert.CertPath;
import java.util.Arrays;
import java.util.List;
@Slf4j
public class TbAdvancedCertificateVerifier implements NewAdvancedCertificateVerifier {
private HandshakeResultHandler resultHandler;
/**
* Get the list of supported certificate types in order of preference.
*
* @return the list of supported certificate types.
* @since 3.0 (renamed from getSupportedCertificateType)
*/
@Override
public List<CertificateType> getSupportedCertificateTypes() {
return Arrays.asList(CertificateType.X_509, CertificateType.RAW_PUBLIC_KEY);
}
/**
* Validates the certificate provided by the the peer as part of the
* certificate message.
* <p>
* If a x509 certificate chain is provided in the certificate message,
* validate the chain and key usage. If a RawPublicKey certificate is
* provided, check, if this public key is trusted.
*
* @param cid connection ID
* @param serverName indicated server names. May be {@code null}, if not
* available or SNI is not enabled.
* @param remotePeer socket address of remote peer
* @param clientUsage indicator to check certificate usage. {@code true},
* check key usage for client, {@code false} for server.
* @param verifySubject {@code true} to verify the certificate's subjects,
* {@code false}, if not.
* @param truncateCertificatePath {@code true} truncate certificate path at
* a trusted certificate before validation.
* @param message certificate message to be validated
* @return certificate verification result, or {@code null}, if result is
* provided asynchronous.
* @since 3.0 (removed DTLSSession session, added remotePeer and
* verifySubject)
*/
@Override
public CertificateVerificationResult verifyCertificate(ConnectionId cid, ServerNames serverName, InetSocketAddress remotePeer,
boolean clientUsage, boolean verifySubject, boolean truncateCertificatePath,
CertificateMessage message) {
CertPath certChain = message.getCertificateChain();
CertificateVerificationResult result;
if (certChain == null) {
PublicKey publicKey = message.getPublicKey();
result = new CertificateVerificationResult(cid, publicKey, null);
} else {
if (message.getCertificateChain().getCertificates().isEmpty()) {
result = new CertificateVerificationResult(cid, new HandshakeException("Empty certificate chain",
new AlertMessage(AlertLevel.FATAL, AlertDescription.BAD_CERTIFICATE)), null);
} else {
result = new CertificateVerificationResult(cid, certChain, null);
}
}
return result;
}
/**
* Return an list of certificate authorities which are trusted
* for authenticating peers.
*
* @return a non-null (possibly empty) list of accepted CA issuers.
*/
@Override
public List<X500Principal> getAcceptedIssuers() {
log.trace("getAcceptedIssuers: return null");
return null;
}
/**
* Set the handler for asynchronous handshake results.
* <p>
* Called during initialization of the {link DTLSConnector}. Synchronous
* implementations may just ignore this using an empty implementation.
*
* @param resultHandler handler for asynchronous master secret results. This
* handler MUST NOT be called from the thread calling
* {@link #verifyCertificate(ConnectionId, ServerNames, InetSocketAddress, boolean, boolean, boolean, CertificateMessage)},
* instead just return the result there.
*/
@Override
public void setResultHandler(HandshakeResultHandler resultHandler) {
if (this.resultHandler != null && resultHandler != null && this.resultHandler != resultHandler) {
throw new IllegalStateException("handshake result handler already set!");
}
this.resultHandler = resultHandler;
}
}

View File

@ -0,0 +1,13 @@
-----BEGIN CERTIFICATE-----
MIIB/TCCAaOgAwIBAgIIVNrVgKT9OE8wCgYIKoZIzj0EAwIwWjEOMAwGA1UEAxMF
Y2YtY2ExFDASBgNVBAsTC0NhbGlmb3JuaXVtMRQwEgYDVQQKEwtFY2xpcHNlIElv
VDEPMA0GA1UEBxMGT3R0YXdhMQswCQYDVQQGEwJDQTAeFw0yMzEwMjYwODA4MjJa
Fw0yNTEwMjUwODA4MjJaMF4xEjAQBgNVBAMTCWNmLWNsaWVudDEUMBIGA1UECxML
Q2FsaWZvcm5pdW0xFDASBgNVBAoTC0VjbGlwc2UgSW9UMQ8wDQYDVQQHEwZPdHRh
d2ExCzAJBgNVBAYTAkNBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEQxYO5/M5
ie6+3QPOaAy5MD6CkFILZwIb2rOBCX/EWPaocX1H+eynUnaEEbmqxeN6rnI/pH19
j4PtsegfHLrzzaNPME0wHQYDVR0OBBYEFKwEDLTJ+5cQoZfbjWN1vJ2ssgK+MAsG
A1UdDwQEAwIHgDAfBgNVHSMEGDAWgBSxVzoI1TL87++hsUb9vQwqODzgUTAKBggq
hkjOPQQDAgNIADBFAiA2KCOw3n2AK9Vm8u2u1bQREIEs3tKAU7eFjpNFn929NwIh
AInhBGoEwS2Xlu5bdZSfWnujoRrEQiIiQpStmLxVcIsH
-----END CERTIFICATE-----

View File

@ -0,0 +1,14 @@
-----BEGIN CERTIFICATE-----
MIICIzCCAcmgAwIBAgIUZZCGYm65c9vU0Xfvd/pAnLVDouUwCgYIKoZIzj0EAwIw
ZzELMAkGA1UEBhMCVUExDTALBgNVBAgMBEtpeXYxDTALBgNVBAcMBEtpeXYxFDAS
BgNVBAoMC1RoaW5nc2JvYXJkMRIwEAYDVQQLDAlkZXZlbG9wZXIxEDAOBgNVBAMM
B2NlcnRfMDEwHhcNMjQxMjE4MTU1NjE1WhcNMjUxMjE4MTU1NjE1WjBnMQswCQYD
VQQGEwJVQTENMAsGA1UECAwES2l5djENMAsGA1UEBwwES2l5djEUMBIGA1UECgwL
VGhpbmdzYm9hcmQxEjAQBgNVBAsMCWRldmVsb3BlcjEQMA4GA1UEAwwHY2VydF8w
MTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNU1tE6o/QpqJJqpy+m+UoPuQe5g
eTgS4M3x0iQS6pzNEJBhzbnOp/BysGMB4wKiAWTRuKdH/gcRXDBTjLd/d7ijUzBR
MB0GA1UdDgQWBBSiao1iNWYzlsrSbxYqbda116HG1jAfBgNVHSMEGDAWgBSiao1i
NWYzlsrSbxYqbda116HG1jAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0gA
MEUCIB2aCM/nvDqic9NkoSX/71GwksLiAKiFNkt2BZQykrcHAiEAr2h5IMdkyurN
Jy/idx2y44CP0tMq/3QV0QLCQFJIi6s=
-----END CERTIFICATE-----

View File

@ -0,0 +1,4 @@
-----BEGIN PRIVATE KEY-----
MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCDn0+4CuLeX7xwBs0ts
UUEDB3+HRwRKdIPeJlIbKuvvEQ==
-----END PRIVATE KEY-----

View File

@ -0,0 +1,8 @@
-----BEGIN EC PARAMETERS-----
BggqhkjOPQMBBw==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIJldU1MBuJUJnNHa9Ob5NGlXc/Os6put9eh1TlIbuScnoAoGCCqGSM49
AwEHoUQDQgAE1TW0Tqj9CmokmqnL6b5Sg+5B7mB5OBLgzfHSJBLqnM0QkGHNuc6n
8HKwYwHjAqIBZNG4p0f+BxFcMFOMt393uA==
-----END EC PRIVATE KEY-----

View File

@ -0,0 +1,35 @@
-----BEGIN CERTIFICATE-----
MIIBaDCCAQ2gAwIBAgIUCY+goBAOhowBs7BHs/qXdAX8XFgwCgYIKoZIzj0EAwIw
ETEPMA0GA1UEAwwGUm9vdENBMB4XDTI0MTIxOTEzNTY1OFoXDTM0MTIxNzEzNTY1
OFowETEPMA0GA1UEAwwGU2VydmVyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
/qief3Kjnz0FpkQVaKRqJq3kHmCqqs+y1EGYLEZZAqLFvxmv7xoL6muG4Mj8tzqk
Ll94JJuz97hG1FiEZsq7O6NDMEEwCwYDVR0PBAQDAgWgMBMGA1UdJQQMMAoGCCsG
AQUFBwMBMB0GA1UdDgQWBBTK/UPsN0I2ErVPILWKMRV6TSeAmTAKBggqhkjOPQQD
AgNJADBGAiEA8EhlOwvTbwGlxo55UIOJp9LBbCp0BEIWojlu8PzOVSsCIQDlV24S
3BUJVCuMRujO5lTfJLxaSKkOEIgRANwIGi88WA==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIBGzCBwgIUP/PGQOKa5EyvsIXNgvv9PNietyEwCgYIKoZIzj0EAwMwEDEOMAwG
A1UEAwwFVFJVU1QwHhcNMjQxMjE5MTM1NjU4WhcNMzQxMjE3MTM1NjU4WjARMQ8w
DQYDVQQDDAZSb290Q0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAT+qJ5/cqOf
PQWmRBVopGomreQeYKqqz7LUQZgsRlkCosW/Ga/vGgvqa4bgyPy3OqQuX3gkm7P3
uEbUWIRmyrs7MAoGCCqGSM49BAMDA0gAMEUCIQD2DY3UDXbzaIBKrsCtohKlEunH
ip9LkSeYfSKCnfm23gIgA8AEJdunpRmPkilxgy6wZSLLROqDpGDnhnyv8dsR8cc=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIBLTCB1AIUcsuauXAqvIS2RQcNPYysETJUAvwwCgYIKoZIzj0EAwMwIzEhMB8G
A1UEAwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTI0MTIxOTEzNTY1OFoX
DTM0MTIxNzEzNTY1OFowEDEOMAwGA1UEAwwFVFJVU1QwWTATBgcqhkjOPQIBBggq
hkjOPQMBBwNCAAT+qJ5/cqOfPQWmRBVopGomreQeYKqqz7LUQZgsRlkCosW/Ga/v
Ggvqa4bgyPy3OqQuX3gkm7P3uEbUWIRmyrs7MAoGCCqGSM49BAMDA0gAMEUCIQCM
DV8sfoArfWiXAUF2LNS3kkHD7sgb91jr2+poEHgBBgIgXf9VeJp3K5jHX6lJwtE8
nd+jW7T9nhTc/5njHg7xons=
-----END CERTIFICATE-----
-----BEGIN EC PARAMETERS-----
BggqhkjOPQMBBw==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIB+Z69so6HqCCWo5VOFxGsLXOlTWIYijOtzt+SeNGrgPoAoGCCqGSM49
AwEHoUQDQgAE/qief3Kjnz0FpkQVaKRqJq3kHmCqqs+y1EGYLEZZAqLFvxmv7xoL
6muG4Mj8tzqkLl94JJuz97hG1FiEZsq7Ow==
-----END EC PRIVATE KEY-----

View File

@ -17,7 +17,6 @@ package org.thingsboard.server.coapserver;
import org.eclipse.californium.core.CoapServer;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.concurrent.ConcurrentMap;
@ -25,5 +24,5 @@ public interface CoapServerService {
CoapServer getCoapServer() throws UnknownHostException;
ConcurrentMap<InetSocketAddress, TbCoapDtlsSessionInfo> getDtlsSessionsMap();
ConcurrentMap<TbCoapDtlsSessionKey, TbCoapDtlsSessionInfo> getDtlsSessionsMap();
}

View File

@ -78,7 +78,7 @@ public class DefaultCoapServerService implements CoapServerService {
}
@Override
public ConcurrentMap<InetSocketAddress, TbCoapDtlsSessionInfo> getDtlsSessionsMap() {
public ConcurrentMap<TbCoapDtlsSessionKey, TbCoapDtlsSessionInfo> getDtlsSessionsMap() {
return tbDtlsCertificateVerifier != null ? tbDtlsCertificateVerifier.getTbCoapDtlsSessionsMap() : null;
}

View File

@ -109,7 +109,8 @@ public class TbCoapDtlsCertificateVerifier implements NewAdvancedCertificateVeri
if (msg != null && strCert.equals(msg.getCredentials())) {
DeviceProfile deviceProfile = msg.getDeviceProfile();
if (msg.hasDeviceInfo() && deviceProfile != null) {
tbCoapDtlsSessionInMemoryStorage.put(remotePeer, new TbCoapDtlsSessionInfo(msg, deviceProfile));
TbCoapDtlsSessionKey tbCoapDtlsSessionKey = new TbCoapDtlsSessionKey(remotePeer, msg.getCredentials());
tbCoapDtlsSessionInMemoryStorage.put(tbCoapDtlsSessionKey, new TbCoapDtlsSessionInfo(msg, deviceProfile));
}
break;
}
@ -138,7 +139,7 @@ public class TbCoapDtlsCertificateVerifier implements NewAdvancedCertificateVeri
public void setResultHandler(HandshakeResultHandler resultHandler) {
}
public ConcurrentMap<InetSocketAddress, TbCoapDtlsSessionInfo> getTbCoapDtlsSessionsMap() {
public ConcurrentMap<TbCoapDtlsSessionKey, TbCoapDtlsSessionInfo> getTbCoapDtlsSessionsMap() {
return tbCoapDtlsSessionInMemoryStorage.getDtlsSessionsMap();
}

View File

@ -18,7 +18,6 @@ package org.thingsboard.server.coapserver;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.net.InetSocketAddress;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@ -26,7 +25,7 @@ import java.util.concurrent.ConcurrentMap;
@Data
public class TbCoapDtlsSessionInMemoryStorage {
private final ConcurrentMap<InetSocketAddress, TbCoapDtlsSessionInfo> dtlsSessionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<TbCoapDtlsSessionKey, TbCoapDtlsSessionInfo> dtlsSessionsMap = new ConcurrentHashMap<>();
private long dtlsSessionInactivityTimeout;
private long dtlsSessionReportTimeout;
@ -36,9 +35,9 @@ public class TbCoapDtlsSessionInMemoryStorage {
this.dtlsSessionReportTimeout = dtlsSessionReportTimeout;
}
public void put(InetSocketAddress remotePeer, TbCoapDtlsSessionInfo dtlsSessionInfo) {
log.trace("DTLS session added to in-memory store: [{}] timestamp: [{}]", remotePeer, dtlsSessionInfo.getLastActivityTime());
dtlsSessionsMap.putIfAbsent(remotePeer, dtlsSessionInfo);
public void put(TbCoapDtlsSessionKey tbCoapDtlsSessionKey, TbCoapDtlsSessionInfo dtlsSessionInfo) {
log.trace("DTLS session added to in-memory store: [{}] timestamp: [{}]", tbCoapDtlsSessionKey, dtlsSessionInfo.getLastActivityTime());
dtlsSessionsMap.putIfAbsent(tbCoapDtlsSessionKey, dtlsSessionInfo);
}
public void evictTimeoutSessions() {

View File

@ -0,0 +1,32 @@
/**
* Copyright © 2016-2024 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.coapserver;
import java.net.InetSocketAddress;
import java.util.Objects;
public record TbCoapDtlsSessionKey(InetSocketAddress peerAddress, String credentials) {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TbCoapDtlsSessionKey that = (TbCoapDtlsSessionKey) o;
return Objects.equals(peerAddress, that.peerAddress) &&
Objects.equals(credentials, that.credentials);
}
}

View File

@ -24,7 +24,10 @@ 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.eclipse.californium.elements.EndpointContext;
import org.eclipse.californium.elements.auth.X509CertPath;
import org.thingsboard.server.coapserver.CoapServerService;
import org.thingsboard.server.coapserver.TbCoapDtlsSessionKey;
import org.thingsboard.server.coapserver.TbCoapDtlsSessionInfo;
import org.thingsboard.server.common.adaptor.AdaptorException;
import org.thingsboard.server.common.adaptor.JsonConverter;
@ -47,12 +50,14 @@ import org.thingsboard.server.transport.coap.client.CoapClientContext;
import org.thingsboard.server.transport.coap.client.TbCoapClientState;
import java.net.InetSocketAddress;
import java.util.Base64;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.security.cert.X509Certificate;
import static org.eclipse.californium.elements.DtlsEndpointContext.KEY_SESSION_ID;
@ -65,7 +70,7 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
private static final int FEATURE_TYPE_POSITION_CERTIFICATE_REQUEST = 3;
private static final int REQUEST_ID_POSITION_CERTIFICATE_REQUEST = 4;
private final ConcurrentMap<InetSocketAddress, TbCoapDtlsSessionInfo> dtlsSessionsMap;
private final ConcurrentMap<TbCoapDtlsSessionKey, TbCoapDtlsSessionInfo> dtlsSessionsMap;
private final long timeout;
private final long piggybackTimeout;
private final CoapClientContext clients;
@ -177,11 +182,7 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
var dtlsSessionId = request.getSourceContext().get(KEY_SESSION_ID);
if (dtlsSessionsMap != null && dtlsSessionId != null && !dtlsSessionId.isEmpty()) {
TbCoapDtlsSessionInfo tbCoapDtlsSessionInfo = dtlsSessionsMap
.computeIfPresent(request.getSourceContext().getPeerAddress(), (dtlsSessionIdStr, dtlsSessionInfo) -> {
dtlsSessionInfo.setLastActivityTime(System.currentTimeMillis());
return dtlsSessionInfo;
});
TbCoapDtlsSessionInfo tbCoapDtlsSessionInfo = this.getCoapDtlsSessionInfo(request.getSourceContext());
if (tbCoapDtlsSessionInfo != null) {
processRequest(exchange, type, request, tbCoapDtlsSessionInfo.getMsg(), tbCoapDtlsSessionInfo.getDeviceProfile());
} else {
@ -251,7 +252,7 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
TransportProtos.SessionInfoProto sessionInfo = clients.getNewSyncSession(clientState);
UUID sessionId = toSessionId(sessionInfo);
transportService.process(sessionInfo, clientState.getAdaptor().convertToPostAttributes(sessionId, request,
clientState.getConfiguration().getAttributesMsgDescriptor()),
clientState.getConfiguration().getAttributesMsgDescriptor()),
new CoapResponseCodeCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
}
@ -259,7 +260,7 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
TransportProtos.SessionInfoProto sessionInfo = clients.getNewSyncSession(clientState);
UUID sessionId = toSessionId(sessionInfo);
transportService.process(sessionInfo, clientState.getAdaptor().convertToPostTelemetry(sessionId, request,
clientState.getConfiguration().getTelemetryMsgDescriptor()),
clientState.getConfiguration().getTelemetryMsgDescriptor()),
new CoapResponseCodeCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
}
@ -458,5 +459,32 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
}
}
private TbCoapDtlsSessionInfo getCoapDtlsSessionInfo(EndpointContext endpointContext) {
InetSocketAddress peerAddress = endpointContext.getPeerAddress();
String certPemStr = getCertPem(endpointContext);
TbCoapDtlsSessionKey tbCoapDtlsSessionKey = StringUtils.isNotBlank(certPemStr) ? new TbCoapDtlsSessionKey(peerAddress, certPemStr) : null;
TbCoapDtlsSessionInfo tbCoapDtlsSessionInfo;
if (tbCoapDtlsSessionKey != null) {
tbCoapDtlsSessionInfo = dtlsSessionsMap
.computeIfPresent(tbCoapDtlsSessionKey, (dtlsSessionIdStr, dtlsSessionInfo) -> {
dtlsSessionInfo.setLastActivityTime(System.currentTimeMillis());
return dtlsSessionInfo;
});
} else {
tbCoapDtlsSessionInfo = null;
}
return tbCoapDtlsSessionInfo;
}
private String getCertPem(EndpointContext endpointContext) {
try {
X509CertPath certPath = (X509CertPath) endpointContext.getPeerIdentity();
X509Certificate x509Certificate = (X509Certificate) certPath.getPath().getCertificates().get(0);
return Base64.getEncoder().encodeToString(x509Certificate.getEncoded());
} catch (Exception e) {
log.error("Failed to get cert PEM: [{}]", endpointContext.getPeerAddress(), e);
return null;
}
}
}