GroupOSCORESubResourceKdcCred.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.security.PrivateKey;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.HashMap;
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.server.resources.CoapExchange;

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

import org.eclipse.californium.cose.CoseException;
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.Util;
import se.sics.ace.coap.CoapReq;
import se.sics.ace.oscore.GroupInfo;
import se.sics.ace.rs.TokenRepository;

/**
 * Definition of the Group OSCORE group-membership sub-resource /kdc-cred
 */
public class GroupOSCORESubResourceKdcCred 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 GroupOSCORESubResourceKdcCred(String resId, Map<String, GroupInfo> existingGroupInfo) {
        
        // set resource identifier
        super(resId);
        
        // set display name
        getAttributes().setTitle("Group OSCORE Group-Membership Sub-Resource \"kdc-cred\" " + 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.
    		CBORObject responseMap = CBORObject.NewMap();
    		
    		CBORObject aceGroupcommError = CBORObject.NewMap();
    		aceGroupcommError.Add(0, GroupcommErrors.ONLY_FOR_GROUP_MEMBERS);
    		responseMap.Add(Constants.PROBLEM_DETAIL_ACE_GROUPCOMM_ERROR, aceGroupcommError);
    		responseMap.Add(Constants.PROBLEM_DETAIL_KEY_TITLE, GroupcommErrors.DESCRIPTION[GroupcommErrors.ONLY_FOR_GROUP_MEMBERS]);
    		
    		byte[] responsePayload = responseMap.EncodeToBytes();
    		exchange.respond(CoAP.ResponseCode.FORBIDDEN,
    						 responsePayload,
    						 Constants.APPLICATION_CONCISE_PROBLEM_DETAILS_CBOR);
    		
    		return;
    	}
        
    	// Respond to the KDC Authentication Credential Request
        
    	CBORObject myResponse = CBORObject.NewMap();
		
		// Authentication Credential of the Group Manager together with proof-of-possession evidence
    	byte[] kdcNonce = new byte[8];
    	new SecureRandom().nextBytes(kdcNonce);
    	myResponse.Add(GroupcommParameters.KDC_NONCE, kdcNonce);
    	
    	CBORObject authCred = CBORObject.FromObject(targetedGroup.getGmAuthCred());
    	
    	myResponse.Add(GroupcommParameters.KDC_CRED, authCred);
    	
    	PrivateKey gmPrivKey;
		try {
			gmPrivKey = targetedGroup.getGmKeyPair().AsPrivateKey();
		} catch (CoseException e) {
			System.err.println("Error when computing the GM PoP evidence " + e.getMessage());
			exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR,
							 "Error when computing the GM PoP evidence");
    		return;
		}
		int signKeyCurve = 0;
		if (targetedGroup.getGmKeyPair().get(KeyKeys.KeyType).AsInt32() == KeyKeys.KeyType_EC2.AsInt32()) {
			signKeyCurve = targetedGroup.getGmKeyPair().get(KeyKeys.EC2_Curve).AsInt32();
		}
		if (targetedGroup.getGmKeyPair().get(KeyKeys.KeyType).AsInt32() == KeyKeys.KeyType_OKP.AsInt32()) {
			signKeyCurve = targetedGroup.getGmKeyPair().get(KeyKeys.OKP_Curve).AsInt32();
		}
		
		
		String cNonceString = TokenRepository.getInstance().getCnonce(subject);
		if(cNonceString == null) {
		    // Return an error response
			System.err.println("Error when retrieving the nonce to use as N_C");
			exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR,
							 "Error when retrieving the nonce to use as N_C");
		    return;
		}
		byte[] cnonce = Base64.getDecoder().decode(cNonceString);
		
		int offset =  0;
		byte[] serializedCNonceCBOR = CBORObject.FromObject(cnonce).EncodeToBytes();
		byte[] serializedGMNonceCBOR = CBORObject.FromObject(kdcNonce).EncodeToBytes();
    	byte[] popInput = new byte[serializedCNonceCBOR.length + serializedGMNonceCBOR.length];
    	System.arraycopy(serializedCNonceCBOR, 0, popInput, offset, serializedCNonceCBOR.length);
    	offset += serializedCNonceCBOR.length;
    	System.arraycopy(serializedGMNonceCBOR, 0, popInput, offset, serializedGMNonceCBOR.length);

		byte[] popEvidence = Util.computeSignature(signKeyCurve, gmPrivKey, popInput);
    	
    	if (popEvidence != null) {
    		myResponse.Add(GroupcommParameters.KDC_CRED_VERIFY, popEvidence);
    	}
    	else {
			exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR,
							 "Error when computing the GM PoP evidence");
    		return;
    	}

    	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;
        }
        
        boolean allowed = false;
    	if (!targetedGroup.isGroupMember(subject)) {
    		
    		// The requester is not a current group member.
    		
    		// 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;
        	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) {
    		// The requester is a group member or is not a signature verifier
    		CBORObject responseMap = CBORObject.NewMap();
    		
    		CBORObject aceGroupcommError = CBORObject.NewMap();
    		aceGroupcommError.Add(0, GroupcommErrors.ONLY_FOR_SIGNATURE_VERIFIERS);
    		responseMap.Add(Constants.PROBLEM_DETAIL_ACE_GROUPCOMM_ERROR, aceGroupcommError);
    		responseMap.Add(Constants.PROBLEM_DETAIL_KEY_TITLE, GroupcommErrors.DESCRIPTION[GroupcommErrors.ONLY_FOR_SIGNATURE_VERIFIERS]);

    		byte[] responsePayload = responseMap.EncodeToBytes();
    		exchange.respond(CoAP.ResponseCode.FORBIDDEN,
    						 responsePayload,
    						 Constants.APPLICATION_CONCISE_PROBLEM_DETAILS_CBOR);
    		
    		return;
    	}
    	
    	if (targetedGroup.getMode() == GroupcommParameters.GROUP_OSCORE_PAIRWISE_MODE_ONLY) {
    		// The group uses only the pairwise mode
    		CBORObject responseMap = CBORObject.NewMap();
    		
    		CBORObject aceGroupcommError = CBORObject.NewMap();
    		aceGroupcommError.Add(0, GroupcommErrors.SIGNATURES_NOT_USED);
    		responseMap.Add(Constants.PROBLEM_DETAIL_ACE_GROUPCOMM_ERROR, aceGroupcommError);
    		responseMap.Add(Constants.PROBLEM_DETAIL_KEY_TITLE, GroupcommErrors.DESCRIPTION[GroupcommErrors.SIGNATURES_NOT_USED]);
    		
    		byte[] responsePayload = responseMap.EncodeToBytes();
    		exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
    						 responsePayload,
    						 Constants.APPLICATION_CONCISE_PROBLEM_DETAILS_CBOR);
    		
    		return;
    	}
    	
    	byte[] requestPayload = exchange.getRequestPayload();
    	
    	if(requestPayload == null) {
    	    exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
    	    				 "A payload must be present");
    	    return;
    	}
    	
    	CBORObject kdcAuthenticationCredentialRequest = CBORObject.DecodeFromBytes(requestPayload);
    	
    	// The payload of the KDC Authentication Credential Request must be a CBOR Map
    	if (!kdcAuthenticationCredentialRequest.getType().equals(CBORType.Map)) {
    	    exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
    	    				 "The payload must be a CBOR map");
    	    return;
    	}
    	
    	if (!kdcAuthenticationCredentialRequest.ContainsKey(GroupcommParameters.CNONCE)) {
    	    exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
    	    				 "Missing parameter: 'cnonce'");
    	    return;
    	}
    	
		// Retrieve the proof-of-possession nonce from the Client
		CBORObject cnonce = kdcAuthenticationCredentialRequest.get(CBORObject.FromObject(GroupcommParameters.CNONCE));
		
		// A client nonce must be included for proof-of-possession
		if (cnonce == null) {
		    exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
		    				 "The parameter 'cnonce' cannot be Null");
		    return;
		}

		// The client nonce must be wrapped in a binary string
		if (!cnonce.getType().equals(CBORType.ByteString)) {
		    exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
		    				 "The parameter 'cnonce' must be a CBOR byte string");
		    return;
		}
    	
    	
    	// Respond to the KDC Authentication Credential Request
        
    	CBORObject myResponse = CBORObject.NewMap();
		
		// Authentication Credential of the Group Manager together with proof-of-possession evidence
    	byte[] kdcNonce = new byte[8];
    	new SecureRandom().nextBytes(kdcNonce);
    	myResponse.Add(GroupcommParameters.KDC_NONCE, kdcNonce);
    	
    	CBORObject authCred = CBORObject.FromObject(targetedGroup.getGmAuthCred());
    	
    	myResponse.Add(GroupcommParameters.KDC_CRED, authCred);
    	
    	PrivateKey gmPrivKey;
		try {
			gmPrivKey = targetedGroup.getGmKeyPair().AsPrivateKey();
		} catch (CoseException e) {
			System.err.println("Error when computing the GM PoP evidence " + e.getMessage());
			exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR,
							 "Error when computing the GM PoP evidence");
    		return;
		}
		int signKeyCurve = 0;
		if (targetedGroup.getGmKeyPair().get(KeyKeys.KeyType).AsInt32() == KeyKeys.KeyType_EC2.AsInt32()) {
			signKeyCurve = targetedGroup.getGmKeyPair().get(KeyKeys.EC2_Curve).AsInt32();
		}
		if (targetedGroup.getGmKeyPair().get(KeyKeys.KeyType).AsInt32() == KeyKeys.KeyType_OKP.AsInt32()) {
			signKeyCurve = targetedGroup.getGmKeyPair().get(KeyKeys.OKP_Curve).AsInt32();
		}
		
		int offset =  0;
		byte[] serializedCNonceCBOR = cnonce.EncodeToBytes();
		byte[] serializedGMNonceCBOR = CBORObject.FromObject(kdcNonce).EncodeToBytes();
    	byte[] popInput = new byte[serializedCNonceCBOR.length + serializedGMNonceCBOR.length];
    	System.arraycopy(serializedCNonceCBOR, 0, popInput, offset, serializedCNonceCBOR.length);
    	offset += serializedCNonceCBOR.length;
    	System.arraycopy(serializedGMNonceCBOR, 0, popInput, offset, serializedGMNonceCBOR.length);

		byte[] popEvidence = Util.computeSignature(signKeyCurve, gmPrivKey, popInput);
    	
    	if (popEvidence != null) {
    		myResponse.Add(GroupcommParameters.KDC_CRED_VERIFY, popEvidence);
    	}
    	else {
			exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR,
							 "Error when computing the GM PoP evidence");
    		return;
    	}

    	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);

    }

}