GroupOSCOREGroupConfigurationResource.java
/*******************************************************************************
* Copyright (c) 2025, RISE AB
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/
package se.sics.ace.oscore.rs.oscoreGroupManager;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.eclipse.californium.core.CoapResource;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.coap.Request;
import org.eclipse.californium.core.coap.Response;
import org.eclipse.californium.core.coap.CoAP.ResponseCode;
import org.eclipse.californium.core.server.resources.CoapExchange;
import org.eclipse.californium.elements.util.Bytes;
import com.upokecenter.cbor.CBORObject;
import com.upokecenter.cbor.CBORType;
import org.eclipse.californium.cose.AlgorithmID;
import org.eclipse.californium.cose.KeyKeys;
import se.sics.ace.AceException;
import se.sics.ace.Constants;
import se.sics.ace.GroupcommErrors;
import se.sics.ace.GroupcommParameters;
import se.sics.ace.GroupcommPolicies;
import se.sics.ace.Util;
import se.sics.ace.coap.CoapReq;
import se.sics.ace.oscore.GroupInfo;
/**
* Definition of the Group OSCORE group-collection resource
*/
public class GroupOSCOREGroupConfigurationResource extends CoapResource {
private final static String rootGroupMembershipResourcePath = "ace-group";
private final static String groupCollectionResourcePath = "manage";
private CBORObject groupConfiguration;
private Map<String, GroupOSCOREGroupConfigurationResource> groupConfigurationResources;
private Map<String, GroupInfo> existingGroupInfo;
/**
* Constructor
* @param resId the resource identifier
* @param groupConfiguration the group configuration, as a CBOR map
* @param existingGroupInfo the set of information of the existing OSCORE groups
*/
public GroupOSCOREGroupConfigurationResource(String resId,
CBORObject groupConfiguration,
Map<String, GroupOSCOREGroupConfigurationResource> groupConfigurationResources,
Map<String, GroupInfo> existingGroupInfo) {
// set resource identifier
super(resId);
// set display name
getAttributes().setTitle("Group OSCORE Group Configuration Resource " + resId);
this.groupConfiguration = groupConfiguration;
this.groupConfigurationResources = groupConfigurationResources;
this.existingGroupInfo = existingGroupInfo;
}
@Override
public synchronized void handleGET(CoapExchange exchange) {
System.out.println("GET request reached the GM at /" + groupCollectionResourcePath + "/" + this.getName());
// Process the request for retrieving the Group Configuration
String subject = null;
String errorString = null;
Request request = exchange.advanced().getCurrentRequest();
try {
subject = CoapReq.getInstance(request).getSenderId();
} catch (AceException e) {
System.err.println("Error while retrieving the client identity: " + e.getMessage());
}
if (subject == null) {
// At this point, this should not really happen, due to the earlier check at the Token Repository
errorString = new String("Unauthenticated client tried to get access");
System.err.println(errorString);
exchange.respond(CoAP.ResponseCode.UNAUTHORIZED, errorString);
return;
}
// Check that at least one scope entry in the access token allows
// the "Read" admin permission for this group-configuration resource
boolean permitted = false;
CBORObject[] adminScopeEntries = Util.getGroupOSCOREAdminPermissionsFromToken(subject, this.getName());
if (adminScopeEntries == null) {
errorString = new String("Operation not permitted");
System.err.println(errorString);
exchange.respond(CoAP.ResponseCode.FORBIDDEN, errorString);
return;
}
for (int i = 0; i < adminScopeEntries.length; i++) {
try {
short permissions = (short) adminScopeEntries[i].get(1).AsInt32();
if (Util.checkGroupOSCOREAdminPermission(permissions, GroupcommParameters.GROUP_OSCORE_ADMIN_READ)) {
permitted = Util.matchingGroupOscoreName(this.getName(), adminScopeEntries[i].get(0));
}
} catch (AceException e) {
System.err.println("Error while verifying the admin permissions: " + e.getMessage());
}
if (permitted) {
break;
}
}
if (!permitted) {
errorString = new String("Operation not permitted");
System.err.println(errorString);
exchange.respond(CoAP.ResponseCode.FORBIDDEN, errorString);
return;
}
// Respond to the request for retrieving the Group Configuration
CBORObject myResponse = CBORObject.NewMap();
for (CBORObject elemKey : this.groupConfiguration.getKeys()) {
myResponse.Add(elemKey, this.groupConfiguration.get(elemKey));
}
// Fill in the response
byte[] responsePayload = myResponse.EncodeToBytes();
Response coapResponse = new Response(CoAP.ResponseCode.CONTENT);
coapResponse.setPayload(responsePayload);
coapResponse.getOptions().setContentFormat(Constants.APPLICATION_ACE_GROUPCOMM_CBOR);
exchange.respond(coapResponse);
}
@Override
public synchronized void handleFETCH(CoapExchange exchange) {
System.out.println("FETCH request reached the GM at /" + groupCollectionResourcePath + "/" + this.getName());
// Process the request for retrieving part of a Group Configuration by filters
String subject = null;
String errorString = null;
Request request = exchange.advanced().getCurrentRequest();
try {
subject = CoapReq.getInstance(request).getSenderId();
} catch (AceException e) {
System.err.println("Error while retrieving the client identity: " + e.getMessage());
}
if (subject == null) {
// At this point, this should not really happen, due to the earlier check at the Token Repository
errorString = new String("Unauthenticated client tried to get access");
System.err.println(errorString);
exchange.respond(CoAP.ResponseCode.UNAUTHORIZED, errorString);
return;
}
// Check that at least one scope entry in the access token allows
// the "Read" admin permission for this group-configuration resource
boolean permitted = false;
CBORObject[] adminScopeEntries = Util.getGroupOSCOREAdminPermissionsFromToken(subject, this.getName());
if (adminScopeEntries == null) {
errorString = new String("Operation not permitted");
System.err.println(errorString);
exchange.respond(CoAP.ResponseCode.FORBIDDEN, errorString);
return;
}
for (int i = 0; i < adminScopeEntries.length; i++) {
try {
short permissions = (short) adminScopeEntries[i].get(1).AsInt32();
if (Util.checkGroupOSCOREAdminPermission(permissions, GroupcommParameters.GROUP_OSCORE_ADMIN_READ)) {
permitted = Util.matchingGroupOscoreName(this.getName(), adminScopeEntries[i].get(0));
}
} catch (AceException e) {
System.err.println("Error while verifying the admin permissions: " + e.getMessage());
}
if (permitted) {
break;
}
}
if (!permitted) {
errorString = new String("Operation not permitted");
System.err.println(errorString);
exchange.respond(CoAP.ResponseCode.FORBIDDEN, errorString);
return;
}
byte[] requestPayload = exchange.getRequestPayload();
if(requestPayload == null || (requestPayload.length == 0)) {
exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
"A payload must be present");
return;
}
if(exchange.getRequestOptions().hasContentFormat() == false ||
exchange.getRequestOptions().getContentFormat() != Constants.APPLICATION_ACE_GROUPCOMM_CBOR) {
errorString = new String("The CoAP option Content-Format must be present, with value application/ace-groupcomm+cbor");
System.err.println(errorString);
exchange.respond(CoAP.ResponseCode.BAD_REQUEST, errorString);
return;
}
CBORObject requestCBOR = CBORObject.DecodeFromBytes(requestPayload);
// The payload of the request must be a CBOR Map
if (!requestCBOR.getType().equals(CBORType.Map)) {
exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
"Invalid payload format");
return;
}
// The CBOR Map in the payload must have only one element
if (requestCBOR.size() != 1) {
exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
"Invalid payload format");
return;
}
// The element of the CBOR Map must be 'conf_filter'
if (requestCBOR.ContainsKey(GroupcommParameters.CONF_FILTER) == false) {
exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
"Invalid payload format");
return;
}
// The 'conf_filter' element of the CBOR Map must be a CBOR array
if (requestCBOR.get(GroupcommParameters.CONF_FILTER).getType() != CBORType.Array) {
exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
"Invalid payload format");
return;
}
// Check that the payload does not contain unexpected parameters
for (CBORObject elem : requestCBOR.getKeys()) {
if (!elem.equals(GroupcommParameters.CONF_FILTER)) {
exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
"Invalid payload format");
System.err.println("XXX4");
return;
}
}
// Respond to the request for retrieving part of a Group Configuration by filters
CBORObject myResponse = CBORObject.NewMap();
// Fill in the response
CBORObject confFilter = requestCBOR.get(GroupcommParameters.CONF_FILTER);
for (int i = 0; i < confFilter.size(); i++) {
CBORObject elemKey = confFilter.get(i);
if (this.groupConfiguration.ContainsKey(elemKey)) {
myResponse.Add(elemKey, this.groupConfiguration.get(elemKey));
}
}
byte[] responsePayload = myResponse.EncodeToBytes();
Response coapResponse = new Response(CoAP.ResponseCode.CONTENT);
coapResponse.setPayload(responsePayload);
coapResponse.getOptions().setContentFormat(Constants.APPLICATION_ACE_GROUPCOMM_CBOR);
exchange.respond(coapResponse);
}
@Override
public synchronized void handlePOST(CoapExchange exchange) {
System.out.println("POST request reached the GM at /" + groupCollectionResourcePath + "/" + this.getName());
// Process the request for overwriting a Group Configuration
String subject = null;
String errorString = null;
Request request = exchange.advanced().getCurrentRequest();
try {
subject = CoapReq.getInstance(request).getSenderId();
} catch (AceException e) {
System.err.println("Error while retrieving the client identity: " + e.getMessage());
}
if (subject == null) {
// At this point, this should not really happen, due to the earlier check at the Token Repository
errorString = new String("Unauthenticated client tried to get access");
System.err.println(errorString);
exchange.respond(CoAP.ResponseCode.UNAUTHORIZED, errorString);
return;
}
// Check that at least one scope entry in the access token allows
// the "Write" admin permission for this group-configuration resource
boolean permitted = false;
CBORObject[] adminScopeEntries = Util.getGroupOSCOREAdminPermissionsFromToken(subject, this.getName());
if (adminScopeEntries == null) {
errorString = new String("Operation not permitted");
System.err.println(errorString);
exchange.respond(CoAP.ResponseCode.FORBIDDEN, errorString);
return;
}
for (int i = 0; i < adminScopeEntries.length; i++) {
try {
short permissions = (short) adminScopeEntries[i].get(1).AsInt32();
if (Util.checkGroupOSCOREAdminPermission(permissions, GroupcommParameters.GROUP_OSCORE_ADMIN_WRITE)) {
permitted = Util.matchingGroupOscoreName(this.getName(), adminScopeEntries[i].get(0));
}
} catch (AceException e) {
System.err.println("Error while verifying the admin permissions: " + e.getMessage());
}
if (permitted) {
break;
}
}
if (!permitted) {
errorString = new String("Operation not permitted");
System.err.println(errorString);
exchange.respond(CoAP.ResponseCode.FORBIDDEN, errorString);
return;
}
byte[] requestPayload = exchange.getRequestPayload();
if(requestPayload == null || (requestPayload.length == 0)) {
exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
"A payload must be present");
return;
}
if(exchange.getRequestOptions().hasContentFormat() == false ||
exchange.getRequestOptions().getContentFormat() != Constants.APPLICATION_ACE_GROUPCOMM_CBOR) {
errorString = new String("The CoAP option Content-Format must be present, with value application/ace-groupcomm+cbor");
System.err.println(errorString);
exchange.respond(CoAP.ResponseCode.BAD_REQUEST, errorString);
return;
}
CBORObject requestCBOR = CBORObject.DecodeFromBytes(requestPayload);
// The payload of the request must be a CBOR Map
if (!requestCBOR.getType().equals(CBORType.Map)) {
errorString = new String("Invalid payload format");
System.err.println(errorString);
exchange.respond(CoAP.ResponseCode.BAD_REQUEST, errorString);
return;
}
// The payload of the request must not include:
// - The configuration parameters 'group_mode' and 'pairwise_mode'
// - The status parameters 'rt', 'ace_groupcomm_profile', 'joining_uri', 'group_name', and 'gid_reuse'
// - The parameters 'conf_filter' and 'app_groups_diff', as not pertaining to this request
if (requestCBOR.getKeys().contains(GroupcommParameters.GROUP_MODE) ||
requestCBOR.getKeys().contains(GroupcommParameters.PAIRWISE_MODE) ||
requestCBOR.getKeys().contains(GroupcommParameters.RT) ||
requestCBOR.getKeys().contains(GroupcommParameters.ACE_GROUPCOMM_PROFILE) ||
requestCBOR.getKeys().contains(GroupcommParameters.JOINING_URI) ||
requestCBOR.getKeys().contains(GroupcommParameters.GROUP_NAME) ||
requestCBOR.getKeys().contains(GroupcommParameters.GID_REUSE) ||
requestCBOR.getKeys().contains(GroupcommParameters.CONF_FILTER) ||
requestCBOR.getKeys().contains(GroupcommParameters.APP_GROUPS_DIFF)) {
errorString = new String("Invalid set of parameters in the request");
System.err.println(errorString);
exchange.respond(CoAP.ResponseCode.BAD_REQUEST, errorString);
return;
}
// This Group Manager does not support RSA an signature algorithm
if (requestCBOR.getKeys().contains(GroupcommParameters.SIGN_ALG)) {
CBORObject signAlg = requestCBOR.get(GroupcommParameters.SIGN_ALG);
if (signAlg.equals(AlgorithmID.RSA_PSS_256.AsCBOR()) ||
signAlg.equals(AlgorithmID.RSA_PSS_384.AsCBOR()) ||
signAlg.equals(AlgorithmID.RSA_PSS_512.AsCBOR())) {
CBORObject myResponse = CBORObject.NewMap();
errorString = new String("RSA is not supported as signature algorithm");
CBORObject aceGroupcommError = CBORObject.NewMap();
aceGroupcommError.Add(0, GroupcommErrors.UNSUPPORTED_GROUP_CONF);
myResponse.Add(Constants.PROBLEM_DETAIL_ACE_GROUPCOMM_ERROR, aceGroupcommError);
myResponse.Add(Constants.PROBLEM_DETAIL_KEY_TITLE, GroupcommErrors.DESCRIPTION[GroupcommErrors.UNSUPPORTED_GROUP_CONF]);
myResponse.Add(Constants.PROBLEM_DETAIL_KEY_DETAIL, errorString);
System.err.println(errorString);
exchange.respond(CoAP.ResponseCode.SERVICE_UNAVAILABLE,
myResponse.EncodeToBytes(),
Constants.APPLICATION_CONCISE_PROBLEM_DETAILS_CBOR);
return;
}
}
for (CBORObject key : requestCBOR.getKeys()) {
if (!GroupcommParameters.isAdminRequestParameterMeaningful(key, requestCBOR.get(key))) {
errorString = new String("Malformed or unrecognized paramemeter with CBOR abbreviation: " + key.AsInt32());
System.err.println(errorString);
exchange.respond(CoAP.ResponseCode.BAD_REQUEST, errorString);
return;
}
}
// Build a the updated version of the group configuration, starting from the current version
CBORObject buildOutput = null;
synchronized (this.groupConfiguration) {
buildOutput = buildGroupConfiguration(requestCBOR, this.groupConfiguration);
// In case of success, update the group configuration
if (buildOutput.size() == 4) {
// For the three parameters 'group_name', 'joining_uri', and 'as_uri', the updated
// configuration always inherits the same value from the current configuration
buildOutput.get(3).Add(GroupcommParameters.GROUP_NAME, groupConfiguration.get(GroupcommParameters.GROUP_NAME));
buildOutput.get(3).Add(GroupcommParameters.JOINING_URI, groupConfiguration.get(GroupcommParameters.JOINING_URI));
buildOutput.get(3).Add(GroupcommParameters.AS_URI, groupConfiguration.get(GroupcommParameters.AS_URI));
this.groupConfiguration = buildOutput.get(3);
}
}
// Update the group information in the corresponding group-membership resource
// Prepare the set of parameters that are relevant for updating the group-memberhsip resource
CBORObject updatedParameterValues = CBORObject.NewMap();
updatedParameterValues.Add(GroupcommParameters.HKDF, this.groupConfiguration.get(GroupcommParameters.HKDF));
updatedParameterValues.Add(GroupcommParameters.CRED_FMT, this.groupConfiguration.get(GroupcommParameters.CRED_FMT));
updatedParameterValues.Add(GroupcommParameters.GP_ENC_ALG, this.groupConfiguration.get(GroupcommParameters.GP_ENC_ALG));
updatedParameterValues.Add(GroupcommParameters.SIGN_ALG, this.groupConfiguration.get(GroupcommParameters.SIGN_ALG));
updatedParameterValues.Add(GroupcommParameters.SIGN_PARAMS, this.groupConfiguration.get(GroupcommParameters.SIGN_PARAMS));
updatedParameterValues.Add(GroupcommParameters.ALG, this.groupConfiguration.get(GroupcommParameters.ALG));
updatedParameterValues.Add(GroupcommParameters.ECDH_ALG, this.groupConfiguration.get(GroupcommParameters.ECDH_ALG));
updatedParameterValues.Add(GroupcommParameters.ECDH_PARAMS, this.groupConfiguration.get(GroupcommParameters.ECDH_PARAMS));
updatedParameterValues.Add(GroupcommParameters.GROUP_POLICIES, this.groupConfiguration.get(GroupcommParameters.GROUP_POLICIES));
updatedParameterValues.Add(GroupcommParameters.MAX_STALE_SETS, this.groupConfiguration.get(GroupcommParameters.MAX_STALE_SETS));
updatedParameterValues.Add(GroupcommParameters.ACTIVE, this.groupConfiguration.get(GroupcommParameters.ACTIVE));
if (this.groupConfiguration.get(GroupcommParameters.DET_HASH_ALG) != null) {
updatedParameterValues.Add(this.groupConfiguration.get(GroupcommParameters.DET_HASH_ALG));
}
CBORObject changedParameters = existingGroupInfo.get(this.getName()).updateGroupInfo(updatedParameterValues);
if (changedParameters == null) {
// This should never happen
errorString = new String("Error while updating the group-membership resource");
System.err.println(errorString);
exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR, errorString);
return;
}
// Depending on the parameters that have been changed, the Group Manager may take particular actions
// Respond to the request for overwriting a Group Configuration
ResponseCode responseCode = CoAP.ResponseCode.valueOf(buildOutput.get(0).AsInt32());
Response coapResponse = new Response(responseCode);
if (buildOutput.get(1) != null) {
int contentFormat = buildOutput.get(1).AsInt32();
coapResponse.getOptions().setContentFormat(contentFormat);
}
byte[] responsePayload = null;
if (buildOutput.get(2) == null) {
responsePayload = Bytes.EMPTY;
}
else if (buildOutput.get(2).getType() == CBORType.Map) {
// Prepare the information to return to the Administrator
CBORObject finalPayloadCBOR = CBORObject.NewMap();
finalPayloadCBOR = buildOutput.get(2);
finalPayloadCBOR.Add(GroupcommParameters.GROUP_NAME, this.groupConfiguration.get(GroupcommParameters.GROUP_NAME));
finalPayloadCBOR.Add(GroupcommParameters.JOINING_URI, this.groupConfiguration.get(GroupcommParameters.JOINING_URI));
finalPayloadCBOR.Add(GroupcommParameters.AS_URI, this.groupConfiguration.get(GroupcommParameters.AS_URI));
responsePayload = buildOutput.get(2).EncodeToBytes();
}
else if (buildOutput.get(2).getType() == CBORType.TextString) {
responsePayload = buildOutput.get(2).AsString().getBytes(Constants.charset);
}
coapResponse.setPayload(responsePayload);
exchange.respond(coapResponse);
}
// TODO - This is just a stub
@Override
public synchronized void handlePATCH(CoapExchange exchange) {
System.out.println("PATCH request reached the GM at /" + groupCollectionResourcePath + "/" + this.getName());
// Process the request for selectively updating a Group Configuration
String subject = null;
String errorString = null;
Request request = exchange.advanced().getCurrentRequest();
try {
subject = CoapReq.getInstance(request).getSenderId();
} catch (AceException e) {
System.err.println("Error while retrieving the client identity: " + e.getMessage());
}
if (subject == null) {
// At this point, this should not really happen, due to the earlier check at the Token Repository
errorString = new String("Unauthenticated client tried to get access");
System.err.println(errorString);
exchange.respond(CoAP.ResponseCode.UNAUTHORIZED, errorString);
return;
}
// Check that at least one scope entry in the access token allows
// the "Write" admin permission for this group-configuration resource
boolean permitted = false;
CBORObject[] adminScopeEntries = Util.getGroupOSCOREAdminPermissionsFromToken(subject, this.getName());
if (adminScopeEntries == null) {
errorString = new String("Operation not permitted");
System.err.println(errorString);
exchange.respond(CoAP.ResponseCode.FORBIDDEN, errorString);
return;
}
for (int i = 0; i < adminScopeEntries.length; i++) {
try {
short permissions = (short) adminScopeEntries[i].get(1).AsInt32();
if (Util.checkGroupOSCOREAdminPermission(permissions, GroupcommParameters.GROUP_OSCORE_ADMIN_WRITE)) {
permitted = Util.matchingGroupOscoreName(this.getName(), adminScopeEntries[i].get(0));
}
} catch (AceException e) {
System.err.println("Error while verifying the admin permissions: " + e.getMessage());
}
if (permitted) {
break;
}
}
if (!permitted) {
errorString = new String("Operation not permitted");
System.err.println(errorString);
exchange.respond(CoAP.ResponseCode.FORBIDDEN, errorString);
return;
}
byte[] requestPayload = exchange.getRequestPayload();
if(requestPayload == null || (requestPayload.length == 0)) {
exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
"A payload must be present");
return;
}
if(exchange.getRequestOptions().hasContentFormat() == false ||
exchange.getRequestOptions().getContentFormat() != Constants.APPLICATION_ACE_GROUPCOMM_CBOR) {
errorString = new String("The CoAP option Content-Format must be present, with value application/ace-groupcomm+cbor");
System.err.println(errorString);
exchange.respond(CoAP.ResponseCode.BAD_REQUEST, errorString);
return;
}
CBORObject requestCBOR = CBORObject.DecodeFromBytes(requestPayload);
// TODO
// Respond to the request for selectively updating a Group Configuration
CBORObject myResponse = CBORObject.NewMap();
// Fill in the response
byte[] responsePayload = myResponse.EncodeToBytes();
Response coapResponse = new Response(CoAP.ResponseCode.CHANGED);
coapResponse.setPayload(responsePayload);
coapResponse.getOptions().setContentFormat(Constants.APPLICATION_ACE_GROUPCOMM_CBOR);
exchange.respond(coapResponse);
}
// TODO - This is just a stub
@Override
public synchronized void handleIPATCH(CoapExchange exchange) {
System.out.println("iPATCH request reached the GM at /" + groupCollectionResourcePath + "/" + this.getName());
// Process the request for selectively updating a Group Configuration
String subject = null;
String errorString = null;
Request request = exchange.advanced().getCurrentRequest();
try {
subject = CoapReq.getInstance(request).getSenderId();
} catch (AceException e) {
System.err.println("Error while retrieving the client identity: " + e.getMessage());
}
if (subject == null) {
// At this point, this should not really happen, due to the earlier check at the Token Repository
errorString = new String("Unauthenticated client tried to get access");
System.err.println(errorString);
exchange.respond(CoAP.ResponseCode.UNAUTHORIZED, errorString);
return;
}
// Check that at least one scope entry in the access token allows
// the "Write" admin permission for this group-configuration resource
boolean permitted = false;
CBORObject[] adminScopeEntries = Util.getGroupOSCOREAdminPermissionsFromToken(subject, this.getName());
if (adminScopeEntries == null) {
errorString = new String("Operation not permitted");
System.err.println(errorString);
exchange.respond(CoAP.ResponseCode.FORBIDDEN, errorString);
return;
}
for (int i = 0; i < adminScopeEntries.length; i++) {
try {
short permissions = (short) adminScopeEntries[i].get(1).AsInt32();
if (Util.checkGroupOSCOREAdminPermission(permissions, GroupcommParameters.GROUP_OSCORE_ADMIN_WRITE)) {
permitted = Util.matchingGroupOscoreName(this.getName(), adminScopeEntries[i].get(0));
}
} catch (AceException e) {
System.err.println("Error while verifying the admin permissions: " + e.getMessage());
}
if (permitted) {
break;
}
}
if (!permitted) {
errorString = new String("Operation not permitted");
System.err.println(errorString);
exchange.respond(CoAP.ResponseCode.FORBIDDEN, errorString);
return;
}
byte[] requestPayload = exchange.getRequestPayload();
if(requestPayload == null || (requestPayload.length == 0)) {
exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
"A payload must be present");
return;
}
if(exchange.getRequestOptions().hasContentFormat() == false ||
exchange.getRequestOptions().getContentFormat() != Constants.APPLICATION_ACE_GROUPCOMM_CBOR) {
errorString = new String("The CoAP option Content-Format must be present, with value application/ace-groupcomm+cbor");
System.err.println(errorString);
exchange.respond(CoAP.ResponseCode.BAD_REQUEST, errorString);
return;
}
CBORObject requestCBOR = CBORObject.DecodeFromBytes(requestPayload);
// TODO
// Respond to the request for selectively updating a Group Configuration
CBORObject myResponse = CBORObject.NewMap();
// Fill in the response
byte[] responsePayload = myResponse.EncodeToBytes();
Response coapResponse = new Response(CoAP.ResponseCode.CHANGED);
coapResponse.setPayload(responsePayload);
coapResponse.getOptions().setContentFormat(Constants.APPLICATION_ACE_GROUPCOMM_CBOR);
exchange.respond(coapResponse);
}
@Override
public synchronized void handleDELETE(CoapExchange exchange) {
System.out.println("DELETE request reached the GM at /" + groupCollectionResourcePath + "/" + this.getName());
// Process the request for deleting a Group Configuration
String subject = null;
String errorString = null;
Request request = exchange.advanced().getCurrentRequest();
try {
subject = CoapReq.getInstance(request).getSenderId();
} catch (AceException e) {
System.err.println("Error while retrieving the client identity: " + e.getMessage());
}
if (subject == null) {
// At this point, this should not really happen, due to the earlier check at the Token Repository
errorString = new String("Unauthenticated client tried to get access");
System.err.println(errorString);
exchange.respond(CoAP.ResponseCode.UNAUTHORIZED, errorString);
return;
}
// Check that at least one scope entry in the access token allows
// the "Delete" admin permission for this group-configuration resource
boolean permitted = false;
CBORObject[] adminScopeEntries = Util.getGroupOSCOREAdminPermissionsFromToken(subject, this.getName());
if (adminScopeEntries == null) {
errorString = new String("Operation not permitted");
System.err.println(errorString);
exchange.respond(CoAP.ResponseCode.FORBIDDEN, errorString);
return;
}
for (int i = 0; i < adminScopeEntries.length; i++) {
try {
short permissions = (short) adminScopeEntries[i].get(1).AsInt32();
if (Util.checkGroupOSCOREAdminPermission(permissions, GroupcommParameters.GROUP_OSCORE_ADMIN_DELETE)) {
permitted = Util.matchingGroupOscoreName(this.getName(), adminScopeEntries[i].get(0));
}
} catch (AceException e) {
System.err.println("Error while verifying the admin permissions: " + e.getMessage());
}
if (permitted) {
break;
}
}
if (!permitted) {
errorString = new String("Operation not permitted");
System.err.println(errorString);
exchange.respond(CoAP.ResponseCode.FORBIDDEN, errorString);
return;
}
// Delete the entry associated with this group configuration
// from the set stored at the group-collection resource
synchronized(groupConfigurationResources) {
this.groupConfigurationResources.remove(this.getName());
}
// Delete the corresponding group-membership resource
synchronized(existingGroupInfo) {
existingGroupInfo.remove(this.getName());
CoapResource res = (CoapResource) this.getParent().getParent().getChild(rootGroupMembershipResourcePath).getChild(this.getName());
if (res != null) {
res.delete();
}
}
// Respond to the request for deleting a Group Configuration
Response coapResponse = new Response(CoAP.ResponseCode.DELETED);
delete();
exchange.respond(coapResponse);
}
/**
* Return the group configuration as a CBOR map
*
* @return the group configuration
*/
public CBORObject getConfigurationParameters() {
return this.groupConfiguration;
}
/**
* Return the default value for a certain parameter
*
* @param paramAbbreviation the abbreviation parameter for which to retrieve the default value, as CBOR integer
* @return the default value, or null in case of invalid parameter
*/
public static CBORObject getDefaultValue(CBORObject paramAbbreviation) {
if (paramAbbreviation.equals(GroupcommParameters.HKDF)) {
return CBORObject.FromObject(AlgorithmID.HMAC_SHA_256.AsCBOR().AsInt32()); // HMAC 256/256 for HKDF SHA-256
}
if (paramAbbreviation.equals(GroupcommParameters.CRED_FMT)) {
return CBORObject.FromObject(Constants.COSE_HEADER_PARAM_KCCS); // CWT Claims Set (CCS)
}
if (paramAbbreviation.equals(GroupcommParameters.MAX_STALE_SETS)) {
return CBORObject.FromObject(3);
}
if (paramAbbreviation.equals(GroupcommParameters.GROUP_MODE)) {
return CBORObject.True;
}
if (paramAbbreviation.equals(GroupcommParameters.GP_ENC_ALG)) {
return CBORObject.FromObject(AlgorithmID.AES_CCM_16_64_128.AsCBOR().AsInt32()); // AES-CCM-16-64-128
}
if (paramAbbreviation.equals(GroupcommParameters.SIGN_ALG)) {
return CBORObject.FromObject(AlgorithmID.EDDSA.AsCBOR().AsInt32Value()); // EdDSA
}
if (paramAbbreviation.equals(GroupcommParameters.DET_REQ)) {
return CBORObject.False;
}
if (paramAbbreviation.equals(GroupcommParameters.DET_HASH_ALG)) {
return CBORObject.FromObject(-16); // SHA-256
}
if (paramAbbreviation.equals(GroupcommParameters.PAIRWISE_MODE)) {
return CBORObject.True;
}
if (paramAbbreviation.equals(GroupcommParameters.ALG)) {
return CBORObject.FromObject(AlgorithmID.AES_CCM_16_64_128.AsCBOR().AsInt32Value()); // AES-CCM-16-64-128
}
if (paramAbbreviation.equals(GroupcommParameters.ECDH_ALG)) {
return CBORObject.FromObject(AlgorithmID.ECDH_SS_HKDF_256.AsCBOR().AsInt32Value()); // ECDH-SS + HKDF-256
}
if (paramAbbreviation.equals(GroupcommParameters.ACTIVE)) {
return CBORObject.False;
}
if (paramAbbreviation.equals(GroupcommParameters.GROUP_DESCRIPTION)) {
return CBORObject.Null;
}
if (paramAbbreviation.equals(GroupcommParameters.GID_REUSE)) {
return CBORObject.False;
}
if (paramAbbreviation.equals(GroupcommParameters.APP_GROUPS)) {
return CBORObject.NewArray();
}
if (paramAbbreviation.equals(GroupcommParameters.GROUP_POLICIES)) {
CBORObject ret = CBORObject.NewMap();
ret.Add(GroupcommPolicies.KEY_CHECK_INTERVAL, 3600);
ret.Add(GroupcommPolicies.EXP_DELTA, 0);
return ret;
}
if (paramAbbreviation.equals(GroupcommParameters.EXP)) {
long currentUnixTime = System.currentTimeMillis() / 1000L;
long lifetime = 3600 * 24 * 365; // Set the lifetime of the group for 1 year from now
CBORObject ret = CBORObject.FromObject(currentUnixTime + lifetime);
return ret;
}
return null;
}
/**
* Return the default value for the 'sign_params' parameter
*
* @param signAlg the value of the parameter sign_alg
* @return the default value, or null in case of invalid parameter
*/
public static CBORObject getDefaultValueSignParams(CBORObject signAlg) {
CBORObject ret = null;
if (signAlg.equals(CBORObject.FromObject(AlgorithmID.EDDSA.AsCBOR()))) {
ret = CBORObject.NewArray();
ret.Add(CBORObject.NewArray());
ret.Add(CBORObject.NewArray());
ret.get(0).Add(KeyKeys.KeyType_OKP);
ret.get(1).Add(KeyKeys.KeyType_OKP);
ret.get(1).Add(KeyKeys.OKP_Ed25519);
return ret;
}
if (signAlg.equals(CBORObject.FromObject(AlgorithmID.ECDSA_256.AsCBOR()))) {
ret = CBORObject.NewArray();
ret.Add(CBORObject.NewArray());
ret.Add(CBORObject.NewArray());
ret.get(0).Add(KeyKeys.KeyType_EC2);
ret.get(1).Add(KeyKeys.KeyType_EC2);
ret.get(1).Add(KeyKeys.EC2_P256);
return ret;
}
if (signAlg.equals(CBORObject.FromObject(AlgorithmID.ECDSA_384.AsCBOR()))) {
ret = CBORObject.NewArray();
ret.Add(CBORObject.NewArray());
ret.Add(CBORObject.NewArray());
ret.get(0).Add(KeyKeys.KeyType_EC2);
ret.get(1).Add(KeyKeys.KeyType_EC2);
ret.get(1).Add(KeyKeys.EC2_P384);
return ret;
}
if (signAlg.equals(CBORObject.FromObject(AlgorithmID.ECDSA_512.AsCBOR()))) {
ret = CBORObject.NewArray();
ret.Add(CBORObject.NewArray());
ret.Add(CBORObject.NewArray());
ret.get(0).Add(KeyKeys.KeyType_EC2);
ret.get(1).Add(KeyKeys.KeyType_EC2);
ret.get(1).Add(KeyKeys.EC2_P521);
return ret;
}
if (signAlg.equals(CBORObject.FromObject(AlgorithmID.RSA_PSS_256.AsCBOR())) ||
signAlg.equals(CBORObject.FromObject(AlgorithmID.RSA_PSS_384.AsCBOR())) ||
signAlg.equals(CBORObject.FromObject(AlgorithmID.RSA_PSS_512.AsCBOR()))) {
ret = CBORObject.NewArray();
ret.Add(CBORObject.NewArray());
ret.Add(CBORObject.NewArray());
ret.get(0).Add(KeyKeys.KeyType_RSA);
ret.get(1).Add(KeyKeys.KeyType_RSA);
return ret;
}
return ret;
}
/**
* Return the default value for the 'ecdh_params' parameter
*
* @param signAlg the value of the parameter sign_alg
* @param groupMode true if the group uses the group mode, or false otherwise
* @return the default value, or null in case of invalid parameter
*/
public static CBORObject getDefaultValueEcdhParams(CBORObject signAlg, boolean groupMode) {
CBORObject ret = null;
if (signAlg.equals(CBORObject.FromObject(AlgorithmID.EDDSA.AsCBOR())) || (groupMode == false)) {
ret = CBORObject.NewArray();
ret.Add(CBORObject.NewArray());
ret.Add(CBORObject.NewArray());
ret.get(0).Add(KeyKeys.KeyType_OKP);
ret.get(1).Add(KeyKeys.KeyType_OKP);
ret.get(1).Add(KeyKeys.OKP_X25519);
return ret;
}
if (signAlg.equals(CBORObject.FromObject(AlgorithmID.ECDSA_256.AsCBOR()))) {
ret = CBORObject.NewArray();
ret.Add(CBORObject.NewArray());
ret.Add(CBORObject.NewArray());
ret.get(0).Add(KeyKeys.KeyType_EC2);
ret.get(1).Add(KeyKeys.KeyType_EC2);
ret.get(1).Add(KeyKeys.EC2_P256);
return ret;
}
if (signAlg.equals(CBORObject.FromObject(AlgorithmID.ECDSA_384.AsCBOR()))) {
ret = CBORObject.NewArray();
ret.Add(CBORObject.NewArray());
ret.Add(CBORObject.NewArray());
ret.get(0).Add(KeyKeys.KeyType_EC2);
ret.get(1).Add(KeyKeys.KeyType_EC2);
ret.get(1).Add(KeyKeys.EC2_P384);
return ret;
}
if (signAlg.equals(CBORObject.FromObject(AlgorithmID.ECDSA_512.AsCBOR()))) {
ret = CBORObject.NewArray();
ret.Add(CBORObject.NewArray());
ret.Add(CBORObject.NewArray());
ret.get(0).Add(KeyKeys.KeyType_EC2);
ret.get(1).Add(KeyKeys.KeyType_EC2);
ret.get(1).Add(KeyKeys.EC2_P521);
return ret;
}
return ret;
}
/**
* Create a preliminary group configuration
*
* @param requestCBOR the payload of the request from the administrator, as a CBOR map
* @param baseConfiguration the current group configuration, if the request was a PUT request to the group-configuration resource;
* or null, if the request was a POST request to the group-collection resource
* @return a CBOR array with up to four elements, in this order
* - The CoAP response code for the response to the Administrator, as a CBOR integer
* - The CoAP Content-Format to use in the response to the Administrator, as a CBOR integer. It can be null
* - The payload for the response to the Administrator, as a CBOR map or a CBOR text string. It can be null
* - Present only in case of success, the preliminary group configuration, as a CBOR map
*
*/
static public CBORObject buildGroupConfiguration(final CBORObject requestCBOR, final CBORObject baseConfiguration) {
CBORObject parameterName;
CBORObject parameterValue = null;
CBORObject newGroupConfiguration = CBORObject.NewMap();
CBORObject ret = CBORObject.NewArray();
int responseCode = -1;
int contentFormat = -1;
CBORObject responsePayload = null;
String errorString = null;
List<Integer> parameterList = new ArrayList<>();
// Configuration parameters
parameterList.add(GroupcommParameters.HKDF.AsInt32());
parameterList.add(GroupcommParameters.CRED_FMT.AsInt32());
parameterList.add(GroupcommParameters.GROUP_MODE.AsInt32());
parameterList.add(GroupcommParameters.GP_ENC_ALG.AsInt32());
parameterList.add(GroupcommParameters.SIGN_ALG.AsInt32());
parameterList.add(GroupcommParameters.SIGN_PARAMS.AsInt32());
parameterList.add(GroupcommParameters.PAIRWISE_MODE.AsInt32());
parameterList.add(GroupcommParameters.ALG.AsInt32());
parameterList.add(GroupcommParameters.ECDH_ALG.AsInt32());
parameterList.add(GroupcommParameters.ECDH_PARAMS.AsInt32());
parameterList.add(GroupcommParameters.DET_REQ.AsInt32());
parameterList.add(GroupcommParameters.DET_HASH_ALG.AsInt32());
// Status parameters
parameterList.add(GroupcommParameters.RT.AsInt32());
parameterList.add(GroupcommParameters.ACTIVE.AsInt32());
parameterList.add(GroupcommParameters.GROUP_NAME.AsInt32());
parameterList.add(GroupcommParameters.GROUP_DESCRIPTION.AsInt32());
parameterList.add(GroupcommParameters.ACE_GROUPCOMM_PROFILE.AsInt32());
parameterList.add(GroupcommParameters.MAX_STALE_SETS.AsInt32());
parameterList.add(GroupcommParameters.EXP.AsInt32());
parameterList.add(GroupcommParameters.GROUP_POLICIES.AsInt32());
parameterList.add(GroupcommParameters.GID_REUSE.AsInt32());
parameterList.add(GroupcommParameters.APP_GROUPS.AsInt32());
parameterList.add(GroupcommParameters.JOINING_URI.AsInt32());
parameterList.add(GroupcommParameters.AS_URI.AsInt32());
for (Integer i : parameterList) {
parameterName = CBORObject.FromObject(i.intValue());
// Some parameters require additional, special handling
boolean omit = false;
boolean postpone = false;
CBORObject forcedValue = null;
if (parameterName.equals(GroupcommParameters.GP_ENC_ALG) ||
parameterName.equals(GroupcommParameters.SIGN_ALG) ||
parameterName.equals(GroupcommParameters.SIGN_PARAMS)) {
if (newGroupConfiguration.get(GroupcommParameters.GROUP_MODE).equals(CBORObject.False)) {
forcedValue = CBORObject.Null;
}
}
else if (parameterName.equals(GroupcommParameters.ALG) ||
parameterName.equals(GroupcommParameters.ECDH_ALG) ||
parameterName.equals(GroupcommParameters.ECDH_PARAMS)) {
if (newGroupConfiguration.get(GroupcommParameters.PAIRWISE_MODE).equals(CBORObject.False)) {
forcedValue = CBORObject.Null;
}
}
else if (parameterName.equals(GroupcommParameters.DET_REQ)) {
if (newGroupConfiguration.get(GroupcommParameters.GROUP_MODE).equals(CBORObject.False) ||
newGroupConfiguration.get(GroupcommParameters.PAIRWISE_MODE).equals(CBORObject.False)) {
omit = true;
}
}
else if (parameterName.equals(GroupcommParameters.DET_HASH_ALG)) {
if ((newGroupConfiguration.ContainsKey(GroupcommParameters.DET_REQ) == false) ||
(newGroupConfiguration.get(GroupcommParameters.DET_REQ).equals(CBORObject.False))) {
omit = true;
}
}
else if (parameterName.equals(GroupcommParameters.RT)) {
forcedValue = CBORObject.FromObject("core.osc.gconf");
}
else if (parameterName.equals(GroupcommParameters.GROUP_NAME)) {
// The group name will be assigned later
postpone = true;
}
else if (parameterName.equals(GroupcommParameters.ACE_GROUPCOMM_PROFILE)) {
forcedValue = CBORObject.FromObject(GroupcommParameters.COAP_GROUP_OSCORE_APP);
}
else if (parameterName.equals(GroupcommParameters.JOINING_URI)) {
// The URI of the group-membership resource will be assigned later
postpone = true;
}
else if (parameterName.equals(GroupcommParameters.AS_URI)) {
// The URI of the associated Authorization Server will be assigned later
// (This Group Manager is not going to accept any Authorization Server
// suggested by the Administrator, and always force its preferred one)
postpone = true;
}
if ((requestCBOR.ContainsKey(parameterName)) && (postpone == false)) {
// The Administrator has specified a value for this parameter,
// which also has to be included now in the group configuration
parameterValue = requestCBOR.get(parameterName);
boolean inconsistentValue = ((forcedValue != null) && (parameterValue.equals(forcedValue) == false));
if (omit || inconsistentValue) {
// The Administrator has specified a parameter that was not supposed to be specified at all,
// or a parameter with a value different than the value that must be taken by such a parameter
errorString = new String(
"1 Invalid use of the parameter with abbreviation' " + parameterName + "'");
responseCode = CoAP.ResponseCode.BAD_REQUEST.value;
contentFormat = Constants.APPLICATION_CONCISE_PROBLEM_DETAILS_CBOR;
responsePayload = CBORObject.NewMap();
CBORObject aceGroupcommError = CBORObject.NewMap();
aceGroupcommError.Add(0, GroupcommErrors.UNSUPPORTED_GROUP_CONF);
responsePayload.Add(Constants.PROBLEM_DETAIL_ACE_GROUPCOMM_ERROR, aceGroupcommError);
responsePayload.Add(Constants.PROBLEM_DETAIL_KEY_TITLE, GroupcommErrors.DESCRIPTION[GroupcommErrors.UNSUPPORTED_GROUP_CONF]);
responsePayload.Add(Constants.PROBLEM_DETAIL_KEY_DETAIL, errorString);
System.err.println(errorString);
break;
}
boolean isMeaningful = GroupcommParameters.isAdminParameterValueMeaningful(parameterName, parameterValue);
if (isMeaningful == false) {
// The value of the parameter is not valid
errorString = new String(
"2 Invalid use of the parameter with abbreviation' " + parameterName + "'");
responseCode = CoAP.ResponseCode.BAD_REQUEST.value;
responsePayload = CBORObject.FromObject(errorString);
System.err.println(errorString);
break;
}
}
else {
if (postpone || omit) {
// This parameter does not have to be included
// now or at all in the group configuration
continue;
}
// This parameter has to be included now in the group configuration
if (forcedValue != null) {
parameterValue = forcedValue;
}
else {
boolean useDefaultValue = true;
if (baseConfiguration != null) {
// This request was sent to overwrite the current group configuration
// These parameters do not change in case of group configuration update.
// They must keep the same value that they have in the current group configuration.
if (parameterName.equals(GroupcommParameters.GROUP_MODE) ||
parameterName.equals(GroupcommParameters.PAIRWISE_MODE) ||
parameterName.equals(GroupcommParameters.GID_REUSE) ||
parameterName.equals(GroupcommParameters.EXP)) {
parameterValue = baseConfiguration.get(parameterName);
useDefaultValue = false;
}
}
if (useDefaultValue == true) {
// Retrieve the default value for this parameter
if (parameterName.equals(GroupcommParameters.SIGN_PARAMS)) {
parameterValue = getDefaultValueSignParams(newGroupConfiguration.get(GroupcommParameters.SIGN_ALG));
}
else if (parameterName.equals(GroupcommParameters.ECDH_PARAMS)) {
boolean groupMode = newGroupConfiguration.get(GroupcommParameters.GROUP_MODE).equals(CBORObject.True) ? true : false;
parameterValue = getDefaultValueEcdhParams(newGroupConfiguration.get(GroupcommParameters.SIGN_ALG), groupMode);
}
else {
parameterValue = getDefaultValue(parameterName);
}
}
if (parameterValue == null) {
// This should never happen
errorString = new String("Error determining the default value for the parameter with abbreviation' " + parameterName + "'");
responseCode = CoAP.ResponseCode.INTERNAL_SERVER_ERROR.value;
responsePayload = CBORObject.FromObject(errorString);
System.err.println(errorString);
break;
}
}
}
// No error has occurred; include the parameter in the group configuration
newGroupConfiguration.Add(parameterName, parameterValue);
}
// Failure
if (responseCode != -1) {
ret.Add(responseCode);
ret.Add((contentFormat == -1) ? null : CBORObject.FromObject(contentFormat));
ret.Add(responsePayload);
}
// Success
else {
responseCode = (baseConfiguration == null) ? CoAP.ResponseCode.CREATED.value : CoAP.ResponseCode.CHANGED.value;
contentFormat = Constants.APPLICATION_ACE_GROUPCOMM_CBOR;
responsePayload = CBORObject.NewMap();
ret.Add(responseCode);
ret.Add(contentFormat);
ret.Add(responsePayload);
ret.Add(newGroupConfiguration);
}
return ret;
}
}