diff --git a/leshan-bsserver-demo/webapp/src/components/bsconfig/ServerInput.vue b/leshan-bsserver-demo/webapp/src/components/bsconfig/ServerInput.vue index 255ad8a595..05e2d7e4b6 100644 --- a/leshan-bsserver-demo/webapp/src/components/bsconfig/ServerInput.vue +++ b/leshan-bsserver-demo/webapp/src/components/bsconfig/ServerInput.vue @@ -27,32 +27,46 @@ class="examplePatch" > + + + + diff --git a/leshan-bsserver-demo/webapp/src/components/wizard/BootstrapServerStep.vue b/leshan-bsserver-demo/webapp/src/components/wizard/BootstrapServerStep.vue index 4a9a85076f..384dad99d1 100644 --- a/leshan-bsserver-demo/webapp/src/components/wizard/BootstrapServerStep.vue +++ b/leshan-bsserver-demo/webapp/src/components/wizard/BootstrapServerStep.vue @@ -15,12 +15,10 @@

This information will be used to add a - LWM2M Bootstrap Server to your LWM2M Client during the bootstrap - Session by writing 1 instance for object /0. -

-

- By default no LWM2M Bootstrap server is added. + LWM2M Bootstrap Server to your LWM2M Client during the + bootstrap Session by writing 1 instance for object /0.

+

By default no LWM2M Bootstrap server is added.

Previous - - Cancel - + Cancel @@ -193,9 +191,9 @@ export default { this.config = { endpoint: null, security: null, - dm: { mode: "no_sec" }, + dm: { security: { mode: "no_sec" } }, bs: null, - toDelete: ["/0", "/1"], + toDelete: ["/0", "/1", "/21"], autoIdForSecurityObject: false, }; this.currentStep = 1; @@ -217,7 +215,7 @@ export default { if (res.dm) { if (!res.dm.url) { res.dm.url = - res.dm.mode == "no_sec" + res.dm.security.mode == "no_sec" ? this.defval.dm.url.nosec : this.defval.dm.url.sec; } @@ -226,24 +224,24 @@ export default { if (res.bs) { if (!res.bs.url) { res.bs.url = - res.bs.mode == "no_sec" + res.bs.security.mode == "no_sec" ? this.defval.bs.url.nosec : this.defval.bs.url.sec; } // apply default rpk value for bs server - if (res.bs.mode == "rpk") { + if (res.bs.security.mode == "rpk") { for (const key in this.defaultrpk) { - if (!res.bs.details[key]) { - res.bs.details[key] = this.defaultrpk[key]; + if (!res.bs.security.details[key]) { + res.bs.security.details[key] = this.defaultrpk[key]; } } } // apply default x509 value for bs server - if (res.bs.mode == "x509") { + if (res.bs.security.mode == "x509") { for (const key in this.defaultx509) { - if (!res.bs.details[key]) { - res.bs.details[key] = this.defaultx509[key]; + if (!res.bs.security.details[key]) { + res.bs.security.details[key] = this.defaultx509[key]; } } } diff --git a/leshan-bsserver-demo/webapp/src/components/wizard/DeleteStep.vue b/leshan-bsserver-demo/webapp/src/components/wizard/DeleteStep.vue index edf7760192..d939996d5a 100644 --- a/leshan-bsserver-demo/webapp/src/components/wizard/DeleteStep.vue +++ b/leshan-bsserver-demo/webapp/src/components/wizard/DeleteStep.vue @@ -18,7 +18,7 @@ existing configuration on the LWM2M client.

- By default, objects /0 and /1 are deleted, + By default, objects /0, /1 and /21 are deleted, then you will be able to define LWM2M Server and LWM2M Bootstrap Server to add.

diff --git a/leshan-bsserver-demo/webapp/src/components/wizard/ServerStep.vue b/leshan-bsserver-demo/webapp/src/components/wizard/ServerStep.vue index b037aa220e..6b9f197f6a 100644 --- a/leshan-bsserver-demo/webapp/src/components/wizard/ServerStep.vue +++ b/leshan-bsserver-demo/webapp/src/components/wizard/ServerStep.vue @@ -58,14 +58,14 @@ export default { data() { return { addServer: true, - internalServer: { mode: "no_sec" }, // internal server Config + internalServer: { security: { mode: "no_sec" } }, // internal server Config }; }, watch: { value(v) { if (!v) { this.addServer = false; - this.internalServer = { mode: "no_sec" }; + this.internalServer = { security: { mode: "no_sec" } }; } else { this.addServer = true; this.internalServer = v; diff --git a/leshan-bsserver-demo/webapp/src/js/bsconfigutil.js b/leshan-bsserver-demo/webapp/src/js/bsconfigutil.js index 1236ec5f52..fdcaa0b769 100644 --- a/leshan-bsserver-demo/webapp/src/js/bsconfigutil.js +++ b/leshan-bsserver-demo/webapp/src/js/bsconfigutil.js @@ -18,16 +18,33 @@ var configFromRestToUI = function (config) { for (var i in config.security) { var security = config.security[i]; if (security.bootstrapServer) { - newConfig.bs.push({ security: security }); + let bs = { security: security }; + + // add oscore object (if any) to bs + let oscoreObjectInstanceId = security.oscoreSecurityMode; + let oscore = config.oscore[oscoreObjectInstanceId]; + if (oscore) { + bs.oscore = oscore; + } + + newConfig.bs.push(bs); } else { // search for DM information; + var server; for (var j in config.servers) { - var server = config.servers[j]; + server = config.servers[j]; if (server.shortId === security.serverId) { newConfig.dm.push(server); server.security = security; } } + + // add oscore object (if any) to dm + let oscoreObjectInstanceId = security.oscoreSecurityMode; + let oscore = config.oscore[oscoreObjectInstanceId]; + if (oscore) { + server.oscore = oscore; + } } } newConfig.toDelete = config.toDelete; @@ -49,10 +66,14 @@ var configFromUIToRest = function (c) { // do a deep copy // we should maybe rather use cloneDeep from lodashz let config = JSON.parse(JSON.stringify(c)); - var newConfig = { servers: {}, security: {} }; + var newConfig = { servers: {}, security: {}, oscore: {} }; for (var i = 0; i < config.bs.length; i++) { var bs = config.bs[i]; newConfig.security[i] = bs.security; + if (bs.oscore) { + newConfig.security[i].oscoreSecurityMode = i; + newConfig.oscore[i] = bs.oscore; + } } if (i == 0) { // To be sure that we are not using instance ID 0 for a DM server. @@ -63,6 +84,11 @@ var configFromUIToRest = function (c) { var dm = config.dm[j]; newConfig.security[i + j] = dm.security; delete dm.security; + if (dm.oscore) { + newConfig.security[i + j].oscoreSecurityMode = i + j; + newConfig.oscore[i + j] = dm.oscore; + delete dm.oscore; + } newConfig.servers[j] = dm; } newConfig.toDelete = config.toDelete; diff --git a/leshan-bsserver-demo/webapp/src/views/Bootstrap.vue b/leshan-bsserver-demo/webapp/src/views/Bootstrap.vue index 21c32e9c1f..e51c146278 100644 --- a/leshan-bsserver-demo/webapp/src/views/Bootstrap.vue +++ b/leshan-bsserver-demo/webapp/src/views/Bootstrap.vue @@ -88,6 +88,15 @@ {{ server.security.securityMode.toLowerCase() }} + + with + + + {{ oscoreIcon() }} + + OSCORE + +
@@ -102,6 +111,15 @@ {{ server.security.securityMode.toLowerCase() }} + + with + + + {{ oscoreIcon() }} + + OSCORE + + @@ -119,7 +137,10 @@ import { configsFromRestToUI, configFromUIToRest } from "../js/bsconfigutil.js"; import { fromHex, fromAscii } from "@leshan-server-core-demo/js/byteutils.js"; import SecurityInfoChip from "@leshan-server-core-demo/components/security/SecurityInfoChip.vue"; import ClientConfigDialog from "../components/wizard/ClientConfigDialog.vue"; -import { getModeIcon } from "@leshan-server-core-demo/js/securityutils.js"; +import { + getModeIcon, + getOscoreIcon, +} from "@leshan-server-core-demo/js/securityutils.js"; export default { components: { ClientConfigDialog, SecurityInfoChip }, @@ -183,28 +204,37 @@ export default { modeIcon(securitymode) { return getModeIcon(securitymode); }, + oscoreIcon() { + return getOscoreIcon(); + }, formatData(c) { let s = {}; - s.securityMode = c.mode.toUpperCase(); + s.securityMode = c.security.mode.toUpperCase(); s.uri = c.url; - switch (c.mode) { + switch (c.security.mode) { case "psk": - s.publicKeyOrId = fromAscii(c.details.identity); - s.secretKey = fromHex(c.details.key); + s.publicKeyOrId = fromAscii(c.security.details.identity); + s.secretKey = fromHex(c.security.details.key); break; case "rpk": - s.publicKeyOrId = fromHex(c.details.client_pub_key); - s.secretKey = fromHex(c.details.client_pri_key); - s.serverPublicKey = fromHex(c.details.server_pub_key); + s.publicKeyOrId = fromHex(c.security.details.client_pub_key); + s.secretKey = fromHex(c.security.details.client_pri_key); + s.serverPublicKey = fromHex(c.security.details.server_pub_key); break; case "x509": - s.publicKeyOrId = fromHex(c.details.client_certificate); - s.secretKey = fromHex(c.details.client_pri_key); - s.serverPublicKey = fromHex(c.details.server_certificate); - s.certificateUsage = c.details.certificate_usage; + s.publicKeyOrId = fromHex(c.security.details.client_certificate); + s.secretKey = fromHex(c.security.details.client_pri_key); + s.serverPublicKey = fromHex(c.security.details.server_certificate); + s.certificateUsage = c.security.details.certificate_usage; break; } + if (c.oscore) { + s.oscore = {}; + s.oscore.oscoreSenderId = fromHex(c.oscore.sid); + s.oscore.oscoreMasterSecret = fromHex(c.oscore.msec); + s.oscore.oscoreRecipientId = fromHex(c.oscore.rid); + } return s; }, @@ -257,6 +287,13 @@ export default { }, }, ]; + if (dmServer.oscore) { + c.dm[0].oscore = { + oscoreSenderId: dmServer.oscore.oscoreSenderId, + oscoreMasterSecret: dmServer.oscore.oscoreMasterSecret, + oscoreRecipientId: dmServer.oscore.oscoreRecipientId, + }; + } } if (config.bs) { let bsServer = this.formatData(config.bs); @@ -278,6 +315,13 @@ export default { }, }, ]; + if (bsServer.oscore) { + c.bs[0].oscore = { + oscoreSenderId: bsServer.oscore.oscoreSenderId, + oscoreMasterSecret: bsServer.oscore.oscoreMasterSecret, + oscoreRecipientId: bsServer.oscore.oscoreRecipientId, + }; + } } if (config.security) { diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/object/Oscore.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/object/Oscore.java index 2c6af3dcb4..8e24e8bab9 100644 --- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/object/Oscore.java +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/object/Oscore.java @@ -163,12 +163,21 @@ public ReadResponse read(ServerIdentity identity, int resourceid) { return ReadResponse.success(resourceid, recipientId); case OSCORE_AEAD_ALGORITHM: + if (aeadAlgorithm == null) { + return ReadResponse.notFound(); + } return ReadResponse.success(resourceid, aeadAlgorithm.getValue()); case OSCORE_HMAC_ALGORITHM: + if (hkdfAlgorithm == null) { + return ReadResponse.notFound(); + } return ReadResponse.success(resourceid, hkdfAlgorithm.getValue()); case OSCORE_MASTER_SALT: + if (masterSalt == null) { + return ReadResponse.notFound(); + } return ReadResponse.success(resourceid, masterSalt); default: diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/ObjectEnabler.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/ObjectEnabler.java index 524f07109e..6589d9bcf2 100644 --- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/ObjectEnabler.java +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/ObjectEnabler.java @@ -366,6 +366,8 @@ public BootstrapDeleteResponse doDelete(ServerIdentity identity, BootstrapDelete if (request.getPath().isRoot() || request.getPath().isObject()) { if (id == LwM2mId.SECURITY) { // For security object, we clean everything except bootstrap Server account. + + // Get bootstrap account and store removed instances ids Entry bootstrapServerAccount = null; int[] instanceIds = new int[instances.size()]; int i = 0; @@ -373,30 +375,73 @@ public BootstrapDeleteResponse doDelete(ServerIdentity identity, BootstrapDelete if (ServersInfoExtractor.isBootstrapServer(instance.getValue())) { bootstrapServerAccount = instance; } else { - // store instance ids + // Store instance ids instanceIds[i] = instance.getKey(); i++; } } + // Clear everything instances.clear(); + + // Put bootstrap account again if (bootstrapServerAccount != null) { instances.put(bootstrapServerAccount.getKey(), bootstrapServerAccount.getValue()); } + fireInstancesRemoved(instanceIds); return BootstrapDeleteResponse.success(); - } else { - instances.clear(); - // fired instances removed - int[] instanceIds = new int[instances.size()]; - int i = 0; - for (Entry instance : instances.entrySet()) { - instanceIds[i] = instance.getKey(); - i++; + } else if (id == LwM2mId.OSCORE) { + // For OSCORE object, we clean everything except OSCORE object link to bootstrap Server account. + + // Get bootstrap account + LwM2mObjectInstance bootstrapInstance = ServersInfoExtractor.getBootstrapSecurityInstance( + getLwm2mClient().getObjectTree().getObjectEnabler(LwM2mId.SECURITY)); + // Get OSCORE instance ID associated to it + Integer bootstrapOscoreInstanceId = bootstrapInstance != null + ? ServersInfoExtractor.getOscoreSecurityMode(bootstrapInstance) + : null; + + // if bootstrap server use OSCORE, + // search the OSCORE instance for this ID and store removed instances ids + if (bootstrapOscoreInstanceId != null) { + Entry bootstrapServerOscore = null; + int[] instanceIds = new int[instances.size()]; + int i = 0; + for (Entry instance : instances.entrySet()) { + if (bootstrapOscoreInstanceId.equals(instance.getKey())) { + bootstrapServerOscore = instance; + } else { + // Store instance ids + instanceIds[i] = instance.getKey(); + i++; + } + } + + // Clear everything + instances.clear(); + + // Put bootstrap OSCORE instance again + if (bootstrapServerOscore != null) { + instances.put(bootstrapServerOscore.getKey(), bootstrapServerOscore.getValue()); + } + fireInstancesRemoved(instanceIds); + return BootstrapDeleteResponse.success(); } - fireInstancesRemoved(instanceIds); + // else delete everything. + } - return BootstrapDeleteResponse.success(); + // In all other cases, just delete everything + instances.clear(); + // fired instances removed + int[] instanceIds = new int[instances.size()]; + int i = 0; + for (Entry instance : instances.entrySet()) { + instanceIds[i] = instance.getKey(); + i++; } + fireInstancesRemoved(instanceIds); + + return BootstrapDeleteResponse.success(); } else if (request.getPath().isObjectInstance()) { if (id == LwM2mId.SECURITY) { // For security object, deleting bootstrap Server account is not allowed @@ -404,6 +449,22 @@ public BootstrapDeleteResponse doDelete(ServerIdentity identity, BootstrapDelete if (ServersInfoExtractor.isBootstrapServer(instance)) { return BootstrapDeleteResponse.badRequest("bootstrap server can not be deleted"); } + } else if (id == LwM2mId.OSCORE) { + // For OSCORE object, deleting instance linked to Bootstrap account is not allowed + + // Get bootstrap instance + LwM2mObjectInstance bootstrapInstance = ServersInfoExtractor.getBootstrapSecurityInstance( + getLwm2mClient().getObjectTree().getObjectEnabler(LwM2mId.SECURITY)); + // Get OSCORE instance ID associated to it + Integer bootstrapOscoreInstanceId = bootstrapInstance != null + ? ServersInfoExtractor.getOscoreSecurityMode(bootstrapInstance) + : null; + + if (bootstrapOscoreInstanceId != null + && bootstrapOscoreInstanceId.equals(request.getPath().getObjectInstanceId())) { + return BootstrapDeleteResponse + .badRequest("OSCORE instance linked to bootstrap server can not be deleted"); + } } if (null != instances.remove(request.getPath().getObjectInstanceId())) { fireInstancesRemoved(request.getPath().getObjectInstanceId()); diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/servers/ServersInfoExtractor.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/servers/ServersInfoExtractor.java index bb02cb440a..f5354c6f77 100644 --- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/servers/ServersInfoExtractor.java +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/servers/ServersInfoExtractor.java @@ -214,6 +214,19 @@ public static DmServerInfo getDMServerInfo(Map obje return info.deviceManagements.get(shortID); } + public static LwM2mObjectInstance getBootstrapSecurityInstance(LwM2mObjectEnabler securityEnabler) { + LwM2mObject securities = (LwM2mObject) securityEnabler.read(SYSTEM, new ReadRequest(SECURITY)).getContent(); + if (securities != null) { + for (LwM2mObjectInstance instance : securities.getInstances().values()) { + if (isBootstrapServer(instance)) { + return instance; + } + } + } + + return null; + } + public static ServerInfo getBootstrapServerInfo(Map objectEnablers) { ServersInfo info = getInfo(objectEnablers); if (info == null) @@ -368,6 +381,14 @@ public static boolean isBootstrapServer(LwM2mInstanceEnabler instance) { return (Boolean) isBootstrap.getValue(); } + public static boolean isBootstrapServer(LwM2mObjectInstance instance) { + LwM2mResource resource = instance.getResource(SEC_BOOTSTRAP); + if (resource == null) { + return false; + } + return (Boolean) resource.getValue(); + } + // OSCORE related methods below public static Integer getOscoreSecurityMode(LwM2mObjectInstance securityInstance) { @@ -390,16 +411,27 @@ public static byte[] getRecipientId(LwM2mObjectInstance oscoreInstance) { } public static long getAeadAlgorithm(LwM2mObjectInstance oscoreInstance) { - return (long) oscoreInstance.getResource(OSCORE_AEAD_ALGORITHM).getValue(); + LwM2mResource resource = oscoreInstance.getResource(OSCORE_AEAD_ALGORITHM); + if (resource != null) + return (long) resource.getValue(); + // return default one from https://datatracker.ietf.org/doc/html/rfc8613#section-3.2 + return AeadAlgorithm.AES_CCM_16_64_128.getValue(); } public static long getHkdfAlgorithm(LwM2mObjectInstance oscoreInstance) { - return (long) oscoreInstance.getResource(OSCORE_HMAC_ALGORITHM).getValue(); + LwM2mResource resource = oscoreInstance.getResource(OSCORE_HMAC_ALGORITHM); + if (resource != null) + return (long) resource.getValue(); + // return default one from https://datatracker.ietf.org/doc/html/rfc8613#section-3.2 + return HkdfAlgorithm.HKDF_HMAC_SHA_256.getValue(); } public static byte[] getMasterSalt(LwM2mObjectInstance oscoreInstance) { - byte[] value = (byte[]) oscoreInstance.getResource(OSCORE_MASTER_SALT).getValue(); + LwM2mResource resource = oscoreInstance.getResource(OSCORE_MASTER_SALT); + if (resource == null) + return null; + byte[] value = (byte[]) resource.getValue(); if (value.length == 0) { return null; } else { diff --git a/leshan-server-core-demo/webapp/src/components/security/SecurityInfoChip.vue b/leshan-server-core-demo/webapp/src/components/security/SecurityInfoChip.vue index 2e80875e3a..6995b984f3 100644 --- a/leshan-server-core-demo/webapp/src/components/security/SecurityInfoChip.vue +++ b/leshan-server-core-demo/webapp/src/components/security/SecurityInfoChip.vue @@ -19,19 +19,23 @@ {{ securityInfo.tls.mode }} - {{$icons.mdiLockOutline}} + {{ oscoreIcon }} oscore
- {{ $icons.mdiLockOpenRemove }} + {{ noSecIcon }} Nothing
diff --git a/leshan-server-core-demo/webapp/src/js/securityutils.js b/leshan-server-core-demo/webapp/src/js/securityutils.js index cbef4adf09..91fd564d5c 100644 --- a/leshan-server-core-demo/webapp/src/js/securityutils.js +++ b/leshan-server-core-demo/webapp/src/js/securityutils.js @@ -13,6 +13,8 @@ import { mdiCertificate, mdiLock, + mdiLockOpenRemove, + mdiLockOutline, mdiKeyChange, mdiHelpRhombusOutline, } from "@mdi/js"; @@ -37,4 +39,12 @@ function getModeIcon(mode) { } } -export { getMode, getModeIcon }; +function getOscoreIcon() { + return mdiLockOutline; +} + +function getNoSecIcon() { + return mdiLockOpenRemove; +} + +export { getMode, getModeIcon, getOscoreIcon, getNoSecIcon };