GroupOSCORESubResourceCreds.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.nio.ByteBuffer;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

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.server.resources.CoapExchange;

import com.upokecenter.cbor.CBORObject;
import com.upokecenter.cbor.CBORType;

import se.sics.ace.AceException;
import se.sics.ace.Constants;
import se.sics.ace.GroupcommParameters;
import se.sics.ace.Util;
import se.sics.ace.coap.CoapReq;
import se.sics.ace.oscore.GroupInfo;

/**
 * Definition of the Group OSCORE group-membership sub-resource /creds
 */
public class GroupOSCORESubResourceCreds extends CoapResource {
	
	private Map<String, GroupInfo> existingGroupInfo = new HashMap<>();
	
	/**
     * Constructor
     * @param resId  the resource identifier
     * @param existingGroupInfo  the set of information of the existing OSCORE groups
     */
    public GroupOSCORESubResourceCreds(String resId, Map<String, GroupInfo> existingGroupInfo) {
        
        // set resource identifier
        super(resId);
        
        // set display name
        getAttributes().setTitle("Group OSCORE Group-Membership Sub-Resource \"creds\" " + resId);
        
        this.existingGroupInfo = existingGroupInfo;
        
    }

    @Override
    public void handleGET(CoapExchange exchange) {
    	System.out.println("GET request reached the GM");
    	
    	// Retrieve the entry for the target group, using the last path segment
    	// of the URI path as the name of the OSCORE group
    	GroupInfo targetedGroup = existingGroupInfo.get(this.getParent().getName());
    	
    	// This should never happen if existing groups are maintained properly
    	if (targetedGroup == null) {
        	exchange.respond(CoAP.ResponseCode.SERVICE_UNAVAILABLE,
        					 "Error when retrieving material for the OSCORE group");
        	return;
    	}
    	
    	String groupName = targetedGroup.getGroupName();
    	
    	// This should never happen if active groups are maintained properly
	  		if (!groupName.equals(this.getParent().getName())) {
        	exchange.respond(CoAP.ResponseCode.SERVICE_UNAVAILABLE,
        					 "Error when retrieving material for the OSCORE group");
				return;
			}
    	
    	String subject = 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
        	exchange.respond(CoAP.ResponseCode.UNAUTHORIZED,
        					 "Unauthenticated client tried to get access");
        	return;
        }
    	
    	if (!targetedGroup.isGroupMember(subject)) {
    		
    		// The requester is not a current group member.
    		//
    		// This is still fine, as long as at least one Access Tokens
    		// of the requester allows also the role "Verifier" in this group
    		
    		// Check that at least one of the Access Tokens for this node
    		// allows (also) the Verifier role for this group
        	
    		int role = 1 << GroupcommParameters.GROUP_OSCORE_VERIFIER;
    		boolean allowed = false;
        	int[] roleSetToken = Util.getGroupOSCORERolesFromToken(subject, groupName);
        	if (roleSetToken == null) {
        		exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR,
        						 "Error when retrieving allowed roles from Access Tokens");
        		return;
        	}
        	else {
        		for (int index = 0; index < roleSetToken.length; index++) {
        			if ((role & roleSetToken[index]) != 0) {
            			// 'scope' in this Access Token admits (also) the role "Verifier" for this group.
        				// This makes it fine for the requester.
        				allowed = true;
        				break;
        			}
        		}
        	}
        	
        	if (!allowed) {
        		exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
        						 "Operation not permitted to a non-member which is not a Verifier");
        		return;
        	}
        	
    	}
        
    	// Respond to the Authentication Credential Request
        
    	CBORObject myResponse = CBORObject.NewMap();
    	
    	CBORObject authCredsArray = CBORObject.NewArray();        	
		CBORObject peerRoles = CBORObject.NewArray();
		CBORObject peerIdentifiers = CBORObject.NewArray();
		
		Map<CBORObject, CBORObject> authCreds = targetedGroup.getAuthCreds();
		
		for (CBORObject sid : authCreds.keySet()) {
			
			// This should never happen; silently ignore
			if (authCreds.get(sid) == null)
				continue;
			
			byte[] peerSenderId = sid.GetByteString();
			// This should never happen; silently ignore
			if (peerSenderId == null)
				continue;
			
			authCredsArray.Add(authCreds.get(sid));
			peerRoles.Add(targetedGroup.getGroupMemberRoles(peerSenderId));
			peerIdentifiers.Add(peerSenderId);
			
		}
		
		myResponse.Add(GroupcommParameters.NUM, CBORObject.FromObject(targetedGroup.getVersion()));

		myResponse.Add(GroupcommParameters.CREDS, authCredsArray);			
		myResponse.Add(GroupcommParameters.PEER_ROLES, peerRoles);
		myResponse.Add(GroupcommParameters.PEER_IDENTIFIERS, peerIdentifiers);

    	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 void handleFETCH(CoapExchange exchange) {
    	System.out.println("FETCH request reached the GM");
    	
    	// Retrieve the entry for the target group, using the last path segment of
    	// the URI path as the name of the OSCORE group
    	GroupInfo targetedGroup = existingGroupInfo.get(this.getParent().getName());
    	
    	// This should never happen if existing groups are maintained properly
    	if (targetedGroup == null) {
        	exchange.respond(CoAP.ResponseCode.SERVICE_UNAVAILABLE,
        					 "Error when retrieving material for the OSCORE group");
        	return;
    	}
    	
    	String groupName = targetedGroup.getGroupName();
    	
    	// This should never happen if active groups are maintained properly
	  		if (!groupName.equals(this.getParent().getName())) {
        	exchange.respond(CoAP.ResponseCode.SERVICE_UNAVAILABLE,
        					 "Error when retrieving material for the OSCORE group");
				return;
			}
    	
    	String subject = 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
        	exchange.respond(CoAP.ResponseCode.UNAUTHORIZED,
        					 "Unauthenticated client tried to get access");
        	return;
        }
    	
    	if (!targetedGroup.isGroupMember(subject)) {
    		
    		// The requester is not a current group member.
    		//
    		// This is still fine, as long as at least one Access Tokens
    		// of the requester allows also the role "Verifier" in this group
    		
    		// Check that at least one of the Access Tokens for this node
    		// allows (also) the Verifier role for this group
        	
    		int role = 1 << GroupcommParameters.GROUP_OSCORE_VERIFIER;
    		boolean allowed = false;
        	int[] roleSetToken = Util.getGroupOSCORERolesFromToken(subject, groupName);
        	if (roleSetToken == null) {
        		exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR,
        						 "Error when retrieving allowed roles from Access Tokens");
        		return;
        	}
        	else {
        		for (int index = 0; index < roleSetToken.length; index++) {
        			if ((role & roleSetToken[index]) != 0) {
            			// 'scope' in this Access Token admits (also) the role "Verifier" for this group.
        				// This makes it fine for the requester.
        				allowed = true;
        				break;
        			}
        		}
        	}
        	
        	if (!allowed) {
        		exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
        						 "Operation not permitted to a non-member which is not a Verifier");
        		return;
        	}
        	
    	}
    	        	
    	byte[] requestPayload = exchange.getRequestPayload();
    	
    	if(requestPayload == null) {
    		exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
    						 "A payload must be present");
    		return;
    	}
    	
    	CBORObject requestCBOR = CBORObject.DecodeFromBytes(requestPayload);
		
    	boolean valid = true;
	    
    	// The payload of the request must be a CBOR Map
    	if (!requestCBOR.getType().equals(CBORType.Map)) {
    		valid = false;
    		
    	}

    	// The CBOR Map must include exactly one element, i.e. 'get_creds'
    	if ((requestCBOR.size() != 1) || (!requestCBOR.ContainsKey(GroupcommParameters.GET_CREDS))) {
    		valid = false;
    		
    	}

    	// Invalid format of 'get_creds'
		if (!valid) {
			exchange.respond(CoAP.ResponseCode.BAD_REQUEST, "Invalid format of 'get_creds'");
    		return;
		}
		
    	// Retrieve 'get_creds'
    	// This parameter must be a CBOR array or the CBOR simple value Null
    	CBORObject getCreds = requestCBOR.get(CBORObject.FromObject((GroupcommParameters.GET_CREDS)));
    	
	    // Invalid format of 'get_creds'
	    if (!getCreds.getType().equals(CBORType.Array) && !getCreds.equals(CBORObject.Null)) {
			exchange.respond(CoAP.ResponseCode.BAD_REQUEST, "Invalid format of 'get_creds'");
    		return;
	    }
			    
	    if (getCreds.getType().equals(CBORType.Array)) {
	    
    		// 'get_creds' must include exactly two elements, both of which CBOR arrays
    		if ( getCreds.size() != 3 ||
    			!getCreds.get(0).getType().equals(CBORType.Boolean) ||
    			!getCreds.get(1).getType().equals(CBORType.Array) ||
    			!getCreds.get(2).getType().equals(CBORType.Array)) {
    			
    			valid = false;
        		
    		}

    		// Invalid format of 'get_creds'
    		if (valid && getCreds.get(1).size() == 0 && getCreds.get(2).size() == 0) {
    			valid = false;
    		}
    		
    		// Invalid format of 'get_creds'
    		if (valid) {
				for (int i = 0; i < getCreds.get(1).size(); i++) {
					// Possible elements of the first array have to be all integers and
					// express a valid combination of roles encoded in the AIF data model
					if (!getCreds.get(1).get(i).getType().equals(CBORType.Integer) ||
						!GroupcommParameters.getValidGroupOSCORERoleCombinations().contains(getCreds.get(1).get(i).AsInt32())) {
							valid = false;
							break;
							
					}
				}
    		}
    		
    		// Invalid format of 'get_creds'
    		if (valid) {
				for (int i = 0; i < getCreds.get(2).size(); i++) {
					// Possible elements of the second array have to be all
					// byte strings, specifying Sender IDs of other group members
					if (!getCreds.get(2).get(i).getType().equals(CBORType.ByteString)) {
						valid = false;
						break;
						
					}			
				}
    		}
			
    		// Invalid format of 'get_creds'
    		if (!valid) {
				exchange.respond(CoAP.ResponseCode.BAD_REQUEST, "Invalid format of 'get_creds'");
	    		return;
    		}
		
	    }
		
		
    	// Respond to the Authentication Credential Request
        
    	CBORObject myResponse = CBORObject.NewMap();
    	
    	CBORObject authCredsArray = CBORObject.NewArray();
		CBORObject peerRoles = CBORObject.NewArray();
		CBORObject peerIdentifiers = CBORObject.NewArray();
		Set<Integer> requestedRoles = new HashSet<Integer>();
		Set<ByteBuffer> requestedSenderIDs = new HashSet<ByteBuffer>();
		
		Map<CBORObject, CBORObject> authCreds = targetedGroup.getAuthCreds();
		
		// Provide the authentication credentials of all the group members
		if (getCreds.equals(CBORObject.Null)) {
			
			for (CBORObject sid : authCreds.keySet()) {
				
    			// This should never happen; silently ignore
    			if (authCreds.get(sid) == null)
    				continue;
    			
    			byte[] memberSenderId = sid.GetByteString();
    			// This should never happen; silently ignore
    			if (memberSenderId == null)
    				continue;

    			int memberRoles = targetedGroup.getGroupMemberRoles(memberSenderId);
    			
    			authCredsArray.Add(authCreds.get(sid));
    			peerRoles.Add(memberRoles);
    			peerIdentifiers.Add(memberSenderId);
    			
			}
			
		}
		// Provide the authentication credentials based on the specified filtering
		else {
		
    		// Retrieve the inclusion flag
			boolean inclusionFlag = getCreds.get(0).getType().equals(CBORType.Boolean);
    		
    		// Retrieve and store the combination of roles specified in the request
    		for (int i = 0; i < getCreds.get(1).size(); i++) {
    			requestedRoles.add((getCreds.get(1).get(i).AsInt32()));
    		}
    		
    		// Retrieve and store the Sender IDs specified in the request
    		for (int i = 0; i < getCreds.get(2).size(); i++) {
    			byte[] myArray = getCreds.get(2).get(i).GetByteString();
    			ByteBuffer myBuffer = ByteBuffer.wrap(myArray);
    			requestedSenderIDs.add(myBuffer);
    		}
		
    		for (CBORObject sid : authCreds.keySet()) {
    			
    			// This should never happen; silently ignore
    			if (authCreds.get(sid) == null)
    				continue;
    			
    			byte[] memberSenderId = sid.GetByteString();
    			// This should never happen; silently ignore
    			if (memberSenderId == null)
    				continue;

    			int memberRoles = targetedGroup.getGroupMemberRoles(memberSenderId);
    			
    			boolean include = false;
    			boolean earlyStop = false;
    			
				for (Integer filter : requestedRoles) {
					int filterRoles = filter.intValue();
					
					// The role(s) of the key owner match with the role filter
					if (filterRoles == (filterRoles & memberRoles)) {
						
						// This authentication credential has to be included anyway,
						// regardless of the Sender ID of the key owner
						if (inclusionFlag) {
							include = true;
						}
						// This authentication credential has to be included only if the Sender ID
						// of the key owner is not in the node identifier filter
						else if (!requestedSenderIDs.contains(ByteBuffer.wrap(memberSenderId))) {
							include = true;
						}
						
						// No need to check the Sender ID filter
						if (include == false) {
							earlyStop = true;
						}
						
						// Stop going through the role filter anyway
						break;
					}	
				}
    			
    			if(!include && !earlyStop) {
    				// This authentication credential has to be included if the Sender ID of
    				// the key owner is in the node identifier filter
    				if (inclusionFlag && requestedSenderIDs.contains(ByteBuffer.wrap(memberSenderId))) {
    					include = true;
    				}
    				// This authentication credential has to be included if the Sender ID of
    				// the key owner is not in the node identifier filter, and the role filter was empty
    				else if (!inclusionFlag && !requestedSenderIDs.contains(ByteBuffer.wrap(memberSenderId))) {
    					if (requestedRoles.size() == 0) {
    						include = true;
    					}
    				}
    			}
    			
    			if (include) {
    				
    				authCredsArray.Add(authCreds.get(sid));
	    			peerRoles.Add(memberRoles);
	    			peerIdentifiers.Add(memberSenderId);
	    			
    			}
    			
    		}
		}
		
		myResponse.Add(GroupcommParameters.NUM, CBORObject.FromObject(targetedGroup.getVersion()));
		
		myResponse.Add(GroupcommParameters.CREDS, authCredsArray);
		myResponse.Add(GroupcommParameters.PEER_ROLES, peerRoles);
		myResponse.Add(GroupcommParameters.PEER_IDENTIFIERS, peerIdentifiers);
    	
    	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);

    }
    
}