added validation for lwm2m device credentials

This commit is contained in:
YevhenBondarenko 2021-09-09 17:04:54 +03:00
parent 3f5e77abb7
commit fe2427e99f
11 changed files with 405 additions and 17 deletions

View File

@ -185,9 +185,9 @@ public class DeviceBulkImportService extends AbstractBulkImportService<Device> {
.filter(Objects::nonNull)
.forEach(securityMode -> {
try {
LwM2MSecurityMode.valueOf(securityMode);
LwM2MSecurityMode.valueOf(securityMode.toUpperCase());
} catch (IllegalArgumentException e) {
throw new DeviceCredentialsValidationException("Unknown LwM2M security mode: " + securityMode);
throw new DeviceCredentialsValidationException("Unknown LwM2M security mode: " + securityMode + ", (the mode should be: NO_SEC, PSK, RPK, X509)!");
}
});

View File

@ -0,0 +1,45 @@
/**
* 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.common.data.device.credentials.lwm2m;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
import org.apache.commons.codec.binary.Hex;
@Getter
@Setter
public abstract class AbstractLwM2MServerCredentialsWithKeys implements LwM2MServerCredentials {
private String clientPublicKeyOrId;
private String clientSecretKey;
@JsonIgnore
public byte[] getDecodedClientPublicKeyOrId() {
return getDecoded(clientPublicKeyOrId);
}
@JsonIgnore
public byte[] getDecodedClientSecretKey() {
return getDecoded(clientSecretKey);
}
@SneakyThrows
private static byte[] getDecoded(String key) {
return Hex.decodeHex(key.toLowerCase().toCharArray());
}
}

View File

@ -0,0 +1,26 @@
/**
* 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.common.data.device.credentials.lwm2m;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class LwM2MBootstrapCredentials {
private LwM2MServerCredentials bootstrapServer;
private LwM2MServerCredentials lwm2mServer;
}

View File

@ -0,0 +1,26 @@
/**
* 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.common.data.device.credentials.lwm2m;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class LwM2MDeviceCredentials {
private LwM2MClientCredentials client;
private LwM2MBootstrapCredentials bootstrap;
}

View File

@ -0,0 +1,37 @@
/**
* 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.common.data.device.credentials.lwm2m;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
property = "securityMode")
@JsonSubTypes({
@JsonSubTypes.Type(value = NoSecServerCredentials.class, name = "NO_SEC"),
@JsonSubTypes.Type(value = PSKServerCredentials.class, name = "PSK"),
@JsonSubTypes.Type(value = RPKServerCredentials.class, name = "RPK"),
@JsonSubTypes.Type(value = X509ServerCredentials.class, name = "X509")
})
@JsonIgnoreProperties(ignoreUnknown = true)
public interface LwM2MServerCredentials {
@JsonIgnore
LwM2MSecurityMode getSecurityMode();
}

View File

@ -0,0 +1,24 @@
/**
* 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.common.data.device.credentials.lwm2m;
public class NoSecServerCredentials implements LwM2MServerCredentials {
@Override
public LwM2MSecurityMode getSecurityMode() {
return LwM2MSecurityMode.NO_SEC;
}
}

View File

@ -0,0 +1,24 @@
/**
* 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.common.data.device.credentials.lwm2m;
public class PSKServerCredentials extends AbstractLwM2MServerCredentialsWithKeys {
@Override
public LwM2MSecurityMode getSecurityMode() {
return LwM2MSecurityMode.PSK;
}
}

View File

@ -0,0 +1,24 @@
/**
* 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.common.data.device.credentials.lwm2m;
public class RPKServerCredentials extends AbstractLwM2MServerCredentialsWithKeys {
@Override
public LwM2MSecurityMode getSecurityMode() {
return LwM2MSecurityMode.RPK;
}
}

View File

@ -0,0 +1,24 @@
/**
* 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.common.data.device.credentials.lwm2m;
public class X509ServerCredentials extends AbstractLwM2MServerCredentialsWithKeys {
@Override
public LwM2MSecurityMode getSecurityMode() {
return LwM2MSecurityMode.X509;
}
}

View File

@ -227,6 +227,10 @@
<groupId>org.elasticsearch.client</groupId>
<artifactId>rest</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.leshan</groupId>
<artifactId>leshan-core</artifactId>
</dependency>
</dependencies>
<build>
<plugins>

View File

@ -16,9 +16,9 @@
package org.thingsboard.server.dao.device;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.codec.binary.Hex;
import org.eclipse.leshan.core.util.SecurityUtil;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
@ -26,10 +26,18 @@ import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MBootstrapCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MClientCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MServerCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.PSKClientCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.PSKServerCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.RPKClientCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.RPKServerCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.X509ClientCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.X509ServerCredentials;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
@ -129,7 +137,7 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
if (StringUtils.isEmpty(mqttCredentials.getClientId()) && StringUtils.isEmpty(mqttCredentials.getUserName())) {
throw new DeviceCredentialsValidationException("Both mqtt client id and user name are empty!");
}
if (StringUtils.isNotEmpty(mqttCredentials.getClientId()) && StringUtils.isNotEmpty(mqttCredentials.getPassword())) {
if (StringUtils.isNotEmpty(mqttCredentials.getClientId()) && StringUtils.isNotEmpty(mqttCredentials.getPassword()) && StringUtils.isEmpty(mqttCredentials.getUserName())) {
throw new DeviceCredentialsValidationException("Password cannot be specified along with client id");
}
@ -154,22 +162,16 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
}
private void formatSimpleLwm2mCredentials(DeviceCredentials deviceCredentials) {
LwM2MClientCredentials clientCredentials;
ObjectNode json;
LwM2MDeviceCredentials lwM2MCredentials;
try {
json = JacksonUtil.fromString(deviceCredentials.getCredentialsValue(), ObjectNode.class);
if (json == null) {
throw new IllegalArgumentException();
}
clientCredentials = JacksonUtil.convertValue(json.get("client"), LwM2MClientCredentials.class);
if (clientCredentials == null) {
throw new IllegalArgumentException();
}
lwM2MCredentials = JacksonUtil.fromString(deviceCredentials.getCredentialsValue(), LwM2MDeviceCredentials.class);
validateLwM2MDeviceCredentials(lwM2MCredentials);
} catch (IllegalArgumentException e) {
throw new DeviceCredentialsValidationException("Invalid credentials body for LwM2M credentials!");
}
String credentialsId = null;
LwM2MClientCredentials clientCredentials = lwM2MCredentials.getClient();
switch (clientCredentials.getSecurityConfigClientMode()) {
case NO_SEC:
@ -185,8 +187,8 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
String cert = EncryptionUtil.trimNewLines(x509Config.getCert());
String sha3Hash = EncryptionUtil.getSha3Hash(cert);
x509Config.setCert(cert);
((ObjectNode) json.get("client")).put("cert", cert);
deviceCredentials.setCredentialsValue(JacksonUtil.toString(json));
((X509ClientCredentials) clientCredentials).setCert(cert);
deviceCredentials.setCredentialsValue(JacksonUtil.toString(lwM2MCredentials));
credentialsId = sha3Hash;
} else {
credentialsId = x509Config.getEndpoint();
@ -199,6 +201,158 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
deviceCredentials.setCredentialsId(credentialsId);
}
private void validateLwM2MDeviceCredentials(LwM2MDeviceCredentials lwM2MCredentials) {
if (lwM2MCredentials == null) {
throw new DeviceCredentialsValidationException("LwM2M credentials should be specified!");
}
LwM2MClientCredentials clientCredentials = lwM2MCredentials.getClient();
if (clientCredentials == null) {
throw new DeviceCredentialsValidationException("LwM2M client credentials should be specified!");
}
validateLwM2MClientCredentials(clientCredentials);
LwM2MBootstrapCredentials bootstrapCredentials = lwM2MCredentials.getBootstrap();
if (bootstrapCredentials == null) {
throw new DeviceCredentialsValidationException("LwM2M bootstrap credentials should be specified!");
}
LwM2MServerCredentials bootstrapServerCredentials = bootstrapCredentials.getBootstrapServer();
if (bootstrapServerCredentials == null) {
throw new DeviceCredentialsValidationException("LwM2M bootstrap server credentials should be specified!");
}
validateServerCredentials(bootstrapServerCredentials, "Bootstrap server");
LwM2MServerCredentials lwm2mServerCredentials = bootstrapCredentials.getLwm2mServer();
if (lwm2mServerCredentials == null) {
throw new DeviceCredentialsValidationException("LwM2M lwm2m server credentials should be specified!");
}
validateServerCredentials(lwm2mServerCredentials, "LwM2M server");
}
private void validateLwM2MClientCredentials(LwM2MClientCredentials clientCredentials) {
if (StringUtils.isEmpty(clientCredentials.getEndpoint())) {
throw new DeviceCredentialsValidationException("LwM2M client endpoint should be specified!");
}
switch (clientCredentials.getSecurityConfigClientMode()) {
case NO_SEC:
break;
case PSK:
PSKClientCredentials pskCredentials = (PSKClientCredentials) clientCredentials;
if (StringUtils.isEmpty(pskCredentials.getIdentity())) {
throw new DeviceCredentialsValidationException("LwM2M client PSK identity should be specified!");
}
String pskKey = pskCredentials.getKey();
if (StringUtils.isEmpty(pskKey)) {
throw new DeviceCredentialsValidationException("LwM2M client PSK key should be specified!");
}
if (!pskKey.matches("-?[0-9a-fA-F]+")) {
throw new DeviceCredentialsValidationException("LwM2M client PSK key should be HexDecimal format!");
}
if (pskKey.length() % 32 != 0 || pskKey.length() > 128) {
throw new DeviceCredentialsValidationException("LwM2M client PSK key must be 32, 64, 128 characters!");
}
break;
case RPK:
RPKClientCredentials rpkCredentials = (RPKClientCredentials) clientCredentials;
if (StringUtils.isEmpty(rpkCredentials.getKey())) {
throw new DeviceCredentialsValidationException("LwM2M client RPK key should be specified!");
}
try {
SecurityUtil.publicKey.decode(rpkCredentials.getDecodedKey());
} catch (Exception e) {
throw new DeviceCredentialsValidationException("LwM2M client RPK key should be in RFC7250 standard!");
}
break;
case X509:
X509ClientCredentials x509CCredentials = (X509ClientCredentials) clientCredentials;
if (x509CCredentials.getCert() != null) {
try {
SecurityUtil.certificate.decode(Hex.decodeHex(x509CCredentials.getCert().toLowerCase().toCharArray()));
} catch (Exception e) {
throw new DeviceCredentialsValidationException("LwM2M client X509 certificate should be in DER-encoded X.509 format!");
}
}
break;
}
}
private void validateServerCredentials(LwM2MServerCredentials serverCredentials, String server) {
switch (serverCredentials.getSecurityMode()) {
case NO_SEC:
break;
case PSK:
PSKServerCredentials pskCredentials = (PSKServerCredentials) serverCredentials;
if (StringUtils.isEmpty(pskCredentials.getClientPublicKeyOrId())) {
throw new DeviceCredentialsValidationException(server + " client PSK public key or id should be specified!");
}
String pskKey = pskCredentials.getClientSecretKey();
if (StringUtils.isEmpty(pskKey)) {
throw new DeviceCredentialsValidationException(server + " client PSK key should be specified!");
}
if (!pskKey.matches("-?[0-9a-fA-F]+")) {
throw new DeviceCredentialsValidationException(server + " client PSK key should be HexDecimal format!");
}
if (pskKey.length() % 32 != 0 || pskKey.length() > 128) {
throw new DeviceCredentialsValidationException(server + " client PSK key must be 32, 64, 128 characters!");
}
break;
case RPK:
RPKServerCredentials rpkCredentials = (RPKServerCredentials) serverCredentials;
if (StringUtils.isEmpty(rpkCredentials.getClientPublicKeyOrId())) {
throw new DeviceCredentialsValidationException(server + " client RPK public key or id should be specified!");
}
try {
SecurityUtil.publicKey.decode(rpkCredentials.getDecodedClientPublicKeyOrId());
} catch (Exception e) {
throw new DeviceCredentialsValidationException(server + " client RPK public key or id should be in RFC7250 standard!");
}
if (StringUtils.isEmpty(rpkCredentials.getClientSecretKey())) {
throw new DeviceCredentialsValidationException(server + " client RPK secret key should be specified!");
}
try {
SecurityUtil.privateKey.decode(rpkCredentials.getDecodedClientSecretKey());
} catch (Exception e) {
throw new DeviceCredentialsValidationException(server + " client RPK secret key should be in RFC5958 standard!");
}
break;
case X509:
X509ServerCredentials x509CCredentials = (X509ServerCredentials) serverCredentials;
if (StringUtils.isEmpty(x509CCredentials.getClientPublicKeyOrId())) {
throw new DeviceCredentialsValidationException(server + " client X509 public key or id should be specified!");
}
try {
SecurityUtil.certificate.decode(x509CCredentials.getDecodedClientPublicKeyOrId());
} catch (Exception e) {
throw new DeviceCredentialsValidationException(server + " client X509 public key or id should be in DER-encoded X.509 format!");
}
if (StringUtils.isEmpty(x509CCredentials.getClientSecretKey())) {
throw new DeviceCredentialsValidationException(server + " client X509 secret key should be specified!");
}
try {
SecurityUtil.privateKey.decode(x509CCredentials.getDecodedClientSecretKey());
} catch (Exception e) {
throw new DeviceCredentialsValidationException(server + " client X509 secret key should be in RFC5958 standard!");
}
break;
}
}
@Override
@CacheEvict(cacheNames = DEVICE_CREDENTIALS_CACHE, key = "'deviceCredentials_' + #deviceCredentials.credentialsId")
public void deleteDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials) {