MessageProcessor.java

/*******************************************************************************
 * Copyright (c) 2020 RISE and others.
 * 
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * and Eclipse Distribution License v1.0 which accompany this distribution.
 * 
 * The Eclipse Public License is available at
 *    http://www.eclipse.org/legal/epl-v20.html
 * and the Eclipse Distribution License is available at
 *    http://www.eclipse.org/org/documents/edl-v10.html.
 * 
 * Contributors:
 *    Marco Tiloca (RISE)
 *    Rikard Höglund (RISE)
 *    
 ******************************************************************************/

package org.eclipse.californium.edhoc;

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.HashMap;
import java.util.Set;

import org.eclipse.californium.core.coap.CoAP.ResponseCode;
import org.eclipse.californium.cose.AlgorithmID;
import org.eclipse.californium.cose.CoseException;
import org.eclipse.californium.cose.HeaderKeys;
import org.eclipse.californium.cose.KeyKeys;
import org.eclipse.californium.cose.OneKey;
import org.eclipse.californium.oscore.HashMapCtxDB;

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

public class MessageProcessor {
	
	private static final boolean debugPrint = true;
	
    /**
     *  Determine the type of a received EDHOC message
     *  
     *  Note: This method DOES NOT recognize EDHOC message_1 as a CoAP response.
     *        This has to be separately handled by the handler of the EDHOC resource
     *        when receiving the "trigger request" from the client.
     *  
     * @param msg   The received EDHOC message, as a CBOR sequence
     * @param isReq   True if the EDHOC message to parse is a CoAP request, false if it is a CoAP response
     * @param edhocSessions   The list of active EDHOC sessions of the recipient
     * @param connectionIdentifier   The connection identifier of this peer.
     *             					 It can be null and then it has to be retrieved from the message
     * @return  The type of the EDHOC message, or -1 if it not a recognized type
     */
	public static int messageType(byte[] msg, boolean isReq,
								  HashMap<CBORObject, EdhocSession> edhocSessions,
								  byte[] connectionIdentifier) {
				
		CBORObject[] myObjects = null;
		
		if (msg == null)
			return -1;
		
		try {
			myObjects = CBORObject.DecodeSequenceFromBytes(msg);
		} catch (CBORException e) {
			System.err.println("Error while parsing the CBOR sequence\n");
			return -1;
		}
		
		if (myObjects == null || myObjects.length == 0)
			return -1;
		
		CBORObject connectionIdentifierCbor = null;
		
		if (isReq == true) {
			// A request always starts with C_X
			CBORObject elem = myObjects[0];
			
			if (elem.equals(CBORObject.True)) {
				// If C_X is equal to 'true' (0xf5), this is EDHOC message_1
				return Constants.EDHOC_MESSAGE_1;
			}
			if (isErrorMessage(myObjects, isReq)) {
				// Check if it is an EDHOC error message
				return Constants.EDHOC_ERROR_MESSAGE;
			}
			
			if (connectionIdentifier != null) {
				connectionIdentifierCbor = CBORObject.FromObject(connectionIdentifier);
			}
			else {
				// The Connection Identifier to retrieve the EDHOC session was not provided.
				// It is present in the message as first element of the CBOR sequence.
				
			    if (elem.getType() == CBORType.ByteString || elem.getType() == CBORType.Integer) {
			    	connectionIdentifier = MessageProcessor.decodeIdentifier(myObjects[0]);
			    	if (connectionIdentifier != null)
						connectionIdentifierCbor = CBORObject.FromObject(connectionIdentifier);
			    }

			}
		}
		
		if (isReq == false) {
			// A response never starts with C_X
			
			if (isErrorMessage(myObjects, isReq)) {
				// Check if it is an EDHOC error message
				return Constants.EDHOC_ERROR_MESSAGE;
			}
			
			// Use the provided Connection Identifier to retrieve the EDHOC session.
			connectionIdentifierCbor = CBORObject.FromObject(connectionIdentifier);
			
		}

	    if (connectionIdentifierCbor != null) {
	    	
	        EdhocSession session = edhocSessions.get(connectionIdentifierCbor);
	        
	        if (session != null) {
	            boolean initiator = session.isInitiator();
	            int currentStep = session.getCurrentStep();
	            boolean clientInitiated = session.isClientInitiated();
	            	            
	            // Take the point of view of the Initiator
	            if (initiator == true) {
	                
	                if (clientInitiated == true) {
		                // The Initiator is the Client
	                    if (isReq == false && currentStep == Constants.EDHOC_SENT_M1)
	                        return Constants.EDHOC_MESSAGE_2;
	                    if (isReq == true && currentStep == Constants.EDHOC_AFTER_M3)
	                        return Constants.EDHOC_MESSAGE_3;
	                    if (isReq == false && currentStep == Constants.EDHOC_SENT_M3)
	                        return Constants.EDHOC_MESSAGE_4;
	                }
	                else {
		                // The Initiator is the Server
	                    if (isReq == true && currentStep == Constants.EDHOC_SENT_M1)
	                        return Constants.EDHOC_MESSAGE_2;
	                	if (isReq == false && currentStep == Constants.EDHOC_AFTER_M3)
	                        return Constants.EDHOC_MESSAGE_3;
	                    if (isReq == true && currentStep == Constants.EDHOC_SENT_M3)
	                        return Constants.EDHOC_MESSAGE_4;
	                }

	            }
	            
	            // Take the point of view of the Responder
	            if (initiator == false) {					

	                if (clientInitiated == true) {
		                // The Responder is the Server
	                    if (isReq == false && currentStep == Constants.EDHOC_AFTER_M2)
	                        return Constants.EDHOC_MESSAGE_2;
	                    if (isReq == true && currentStep == Constants.EDHOC_SENT_M2)
	                        return Constants.EDHOC_MESSAGE_3;
	                    if (isReq == false && currentStep == Constants.EDHOC_AFTER_M4)
	                        return Constants.EDHOC_MESSAGE_4;
	                }
	                else {
		                // The Responder is the Client
	                    if (isReq == true && currentStep == Constants.EDHOC_AFTER_M2)
	                        return Constants.EDHOC_MESSAGE_2;
	                    if (isReq == false && currentStep == Constants.EDHOC_SENT_M2)
	                        return Constants.EDHOC_MESSAGE_3;
	                    if (isReq == true && currentStep == Constants.EDHOC_AFTER_M4)
	                        return Constants.EDHOC_MESSAGE_4;
	                }
	            
	            }
	            
	        }

	    }

		return -1;
		
	}
	
    /**
     *  Determine if a message is an EDHOC error message
     * @param myObjects   The message to check, as an array of CBOR objects extracted from a CBOR sequence
     * @param isReq   True if the message is a request, false otherwise
     * @return  True if it is an EDHOC error message, or false otherwise
     */
	public static boolean isErrorMessage(CBORObject[] myObjects, boolean isReq) {
		
		// A CoAP message including an EDHOC error message is a CBOR sequence of at least two elements
		if (myObjects.length < 2)
			return false;
		
		if (isReq == true) {
			// If in a request, this starts with C_X different than 'true' (0xf5),
			// followed by ERR_CODE as a CBOR integer
			if (!myObjects[0].equals(CBORObject.True) && myObjects[1].getType() == CBORType.Integer)
				return true;
			
		}
		else {
			// If in a response, this starts with ERR_CODE as a CBOR integer
			if (myObjects[0].getType() == CBORType.Integer)
				return true;
		}
		
		return false;
		
	}
	
    /**
     *  Determine if a message is an EDHOC error message
     * @param msg   The message to check, as a CBOR sequence
     * @param isReq   True if the message is a request, false otherwise
     * @return  True if it is an EDHOC error message, or false otherwise
     */
	public static boolean isErrorMessage(byte[] msg, boolean isReq) {
		
		CBORObject[] myObjects = null;
		
		try {
			myObjects = CBORObject.DecodeSequenceFromBytes(msg);
		} catch (CBORException e) {
			System.err.println("Error while parsing the CBOR sequence\n");
			return false;
		}
		
		if (myObjects == null)
			return false;
		
		return isErrorMessage(myObjects, isReq);
		
	}
	
    /**
     *  Process an EDHOC Message 1
     * @param sequence   The CBOR sequence used as payload of the EDHOC Message 1
     * @param isReq   True if the CoAP message is a request, or False otherwise
     * @param supportedCipherSuites   The list of cipher suites supported by this peer
     * @param supportedEADs   The list of EAD items supported by this peer
     * @param appProfile   The application profile to use
     * @param sessions   The EDHOC sessions of this peer
     * @param sideProcessor   The side processor object to use for this message
     * @return   A list of CBOR Objects including up to two elements.
     * 
     *           The first element is a CBOR byte string, with value either:
     *              i) a zero length byte string, indicating that the EDHOC message_1 was successfully processed; or
     *             ii) a non-zero length byte string as the EDHOC Error Message to be sent.
     *
     *           In case (ii), the second element is a CBOR integer, with value the CoAP response code
     *           to use for the EDHOC Error Message, if this is a CoAP response.
     */
	public static List<CBORObject> readMessage1(byte[] sequence, boolean isReq,
												List<Integer> supportedCipherSuites,
												Set<Integer> supportedEADs,
												AppProfile appProfile,
												SideProcessor sideProcessor) {
		
		if (sequence == null || supportedCipherSuites == null)
				return null;
		
		CBORObject[] ead1 = null; // Will be set if External Authorization Data is present as EAD1
		
		List<CBORObject> processingResult = new ArrayList<CBORObject>(); // List of CBOR Objects to return as result
		
		// Serialization of the message to be sent back to the Initiator
		byte[] replyPayload = new byte[] {};
		
		boolean error = false; // Will be set to True if an EDHOC Error Message has to be returned
		int errorCode = Constants.ERR_CODE_UNSPECIFIED_ERROR; // The error code to use for the EDHOC Error Message
		ResponseCode responseCode = null; // Will be set to the CoAP response code to use for the EDHOC Error Message
		String errMsg = null; // The text string to be possibly returned as DIAG_MSG in an EDHOC Error Message
		CBORObject cI = null; // The Connection Identifier C_I as encoded in the EDHOC message
		CBORObject suitesR = null; // The SUITE_R element to be possibly returned as SUITES_R in an EDHOC Error Message

		
		int index = -1;	
		CBORObject[] objectList = null;
		try {
			objectList = CBORObject.DecodeSequenceFromBytes(sequence);
		}
		catch (Exception e) {
		    errMsg = new String("Malformed or invalid EDHOC message_1");
		    responseCode = ResponseCode.BAD_REQUEST;
		    error = true;
		}
		
		/* Consistency checks */
		
		if (error == false && objectList.length == 0) {
		    errMsg = new String("Malformed or invalid EDHOC message_1");
		    responseCode = ResponseCode.BAD_REQUEST;
		    error = true;
		}
		
    	if (error == false && appProfile == null) {
			errMsg = new String("Impossible to retrieve the application profile");
			responseCode = ResponseCode.BAD_REQUEST;
			error = true;
    	}
    	if (error == false) {
    	    // If the received message is a request (i.e. the CoAP client is the initiator), the first element
    	    // before the actual message_1 is the CBOR simple value 'true', i.e. the byte 0xf5, and it can be skipped
	    	if (isReq) {
	    		index++;
	    		if (!objectList[index].equals(CBORObject.True)) {
	    			errMsg = new String("The first element must be the CBOR simple value 'true'");
	    			responseCode = ResponseCode.BAD_REQUEST;
	    			error = true;
	    		}
	        }
    	}
		
    	
		// METHOD
    	index++;
    	int method = -1;
    	if (error == false) {
			if (objectList[index].getType() != CBORType.Integer) {
				errMsg = new String("METHOD must be an integer");
				responseCode = ResponseCode.BAD_REQUEST;
				error = true;
			}
			else {
				// Check that the indicated authentication method is supported
		    	method = objectList[index].AsInt32();
		    	if (!appProfile.isAuthMethodSupported(method)) {
					errMsg = new String("Authentication method " + method + " is not supported");
					responseCode = ResponseCode.BAD_REQUEST;
					error = true;
		    	}
			}
		}
		
		// SUITES_I
		index++;
		int indexCredI = index;
		if (error == false &&
			objectList[index].getType() != CBORType.Integer &&
			objectList[index].getType() != CBORType.Array) {
				errMsg = new String("SUITES_I must be an integer or an array");
				responseCode = ResponseCode.BAD_REQUEST;
				error = true;
		}
		if (error == false &&
			objectList[index].getType() == CBORType.Integer &&
			objectList[index].AsInt32() < 0) {
				errMsg = new String("SUITES_I as an integer must be greater than 0");
				responseCode = ResponseCode.BAD_REQUEST;
				error = true;
		}
		if (error == false &&
			objectList[index].getType() == CBORType.Array) {
				if (objectList[index].size() < 2) {
					errMsg = new String("SUITES_I as an array must have at least 2 elements");
					responseCode = ResponseCode.BAD_REQUEST;
					error = true;
				}
				else {
					for (int i = 0; i < objectList[index].size(); i++) {
						if(objectList[index].get(i).getType() != CBORType.Integer) {
							errMsg = new String("SUITES_I as an array must have integers as elements");
							responseCode = ResponseCode.BAD_REQUEST;
							error = true;
							break;
						}
						if(objectList[index].get(i).AsInt32() < 0) {
							errMsg = new String("SUITES_I as an array must have integers greater than 0");
							responseCode = ResponseCode.BAD_REQUEST;
							error = true;
							break;
						}
					}
				}
		}

		// Check if the selected cipher suite is supported and that no prior cipher suite in SUITES_I is supported
		List<Integer> cipherSuitesToOffer = new ArrayList<Integer>();
		if (error == false) {
			int selectedCipherSuite;
			
			if (objectList[index].getType() == CBORType.Integer) {
				// SUITES_I is the selected cipher suite
				selectedCipherSuite = objectList[index].AsInt32();
				
				// This peer does not support the selected cipher suite
				if (!supportedCipherSuites.contains(Integer.valueOf(selectedCipherSuite))) {
					errMsg = new String("The selected cipher suite is not supported");
					errorCode = Constants.ERR_CODE_WRONG_SELECTED_CIPHER_SUITE;
					responseCode = ResponseCode.BAD_REQUEST;
					error = true;
					
					// SUITE_R will include all the cipher suites supported by the Responder
					cipherSuitesToOffer = supportedCipherSuites;
				}
			}
			
			else if (objectList[index].getType() == CBORType.Array) {
				// The selected cipher suite is the last element of SUITES_I
				int size = objectList[index].size(); 
				selectedCipherSuite = objectList[index].get(size-1).AsInt32();
				
				int firstSharedCipherSuite = -1;
				// Find the first commonly supported cipher suite, i.e. the cipher suite both
				// supported by the Responder and specified as early as possible in SUITES_I
				for (int i = 0; i < size; i++) {
					int suite = objectList[index].get(i).AsInt32();
					if (supportedCipherSuites.contains(Integer.valueOf(suite))) {
						firstSharedCipherSuite = suite;
						break;
					}
				}
				
				if (!supportedCipherSuites.contains(Integer.valueOf(selectedCipherSuite))) {
					// The Responder does not support the selected cipher suite
					
					errMsg = new String("The selected cipher suite is not supported");
					errorCode = Constants.ERR_CODE_WRONG_SELECTED_CIPHER_SUITE;
					responseCode = ResponseCode.BAD_REQUEST;
					error = true;
					
					if (firstSharedCipherSuite == -1) {
						// The Responder does not support any cipher suites in SUITE_I.
						// SUITE_R will include all the cipher suites supported by the Responder
						cipherSuitesToOffer = supportedCipherSuites;
					}
					else {
						// SUITES_R will include only the cipher suite supported
						// by both peers and most preferred by the Initiator.
						cipherSuitesToOffer.add(firstSharedCipherSuite);
					}
					
				}
				else if (firstSharedCipherSuite != selectedCipherSuite) {
					// The Responder supports the selected cipher suite, but it has to reply with an EDHOC Error Message
					// if it supports a cipher suite more preferred by the Initiator than the selected cipher suite
					
					errMsg = new String("A cipher suite more preferred than the selected cipher suite is also supported");
					errorCode = Constants.ERR_CODE_WRONG_SELECTED_CIPHER_SUITE;
					responseCode = ResponseCode.BAD_REQUEST;
					error = true;
					
					// SUITES_R will include only the cipher suite supported
					// by both peers and most preferred by the Initiator.
					cipherSuitesToOffer.add(firstSharedCipherSuite);
					
				}
				
			}
			
		}
		
		// G_X
		index++;
		int indexGx = index;
		if (error == false &&
			objectList[index].getType() != CBORType.ByteString) {
				errMsg = new String("G_X must be a byte string");
				responseCode = ResponseCode.BAD_REQUEST;
				error = true;
		}
		
		// C_I
		index++;
		if (error == false &&
			objectList[index].getType() != CBORType.ByteString &&
			objectList[index].getType() != CBORType.Integer) {
				errMsg = new String("C_I must be a byte string or an integer");
				responseCode = ResponseCode.BAD_REQUEST;
				error = true;
		}
		
		if (error == false && decodeIdentifier(objectList[index]) == null) {
			errMsg = new String("Invalid encoding of C_I");
			responseCode = ResponseCode.BAD_REQUEST;
			error = true;
		}

		byte[] connectionIdentifierInitiator = null;
		if (error == false) {
			cI = objectList[index];
			
			connectionIdentifierInitiator = decodeIdentifier(cI);
			if (debugPrint) {
			    Util.nicePrint("Connection Identifier of the Initiator", connectionIdentifierInitiator);
			    Util.nicePrint("C_I", cI.EncodeToBytes());
			}
		}
		
		index++;
		// EAD_1
		if (error == false && objectList.length > index) {
		    // EAD_1 is present
			ead1 = preParseEAD(objectList, index, 1, supportedEADs);
			
			if (ead1.length == 1 && ead1[0].getType() == CBORType.TextString) {
		        errMsg = new String(ead1[0].toString());
		        responseCode = ResponseCode.BAD_REQUEST;
		        ead1 = null;
		        error = true;
			}
		}
		
		// Invoke the processing of EAD items in EAD_1 (if any)
		if (error == false && ead1!= null && ead1.length != 0) {
			
	 	    CBORObject[] sideProcessorInfo = new CBORObject[4];

	 	   sideProcessorInfo[0] = CBORObject.FromObject(method);
		    
		    if (objectList[indexCredI].getType() == CBORType.Integer) {
		    	// CRED_I was a single CBOR integer, so a CBOR array including only that integer is built.
		    	// Such an integer also indicates the selected cipher suite.
		    	sideProcessorInfo[1] = CBORObject.NewArray();
		    	sideProcessorInfo[1].Add(objectList[indexCredI]);
		    }
		    else {
		    	// CRED_I was a CBOR array of integers and can be taken as is.
		    	// The last integer in the array indicates the selected cipher suite.
		    	sideProcessorInfo[1] = objectList[indexCredI];
		    }
		    
		    sideProcessorInfo[2] = objectList[indexGx];
		    sideProcessorInfo[3] = CBORObject.FromObject(connectionIdentifierInitiator);

			sideProcessor.sideProcessingMessage1(sideProcessorInfo, ead1);
			
			// A fatal error occurred
			if (sideProcessor.getResults(Constants.EDHOC_MESSAGE_1, false).
					containsKey(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_ERROR))) {
				errMsg = new String(sideProcessor.getResults(Constants.EDHOC_MESSAGE_1, false).
						get(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_ERROR)).get(0).
						get(Integer.valueOf(Constants.SIDE_PROCESSOR_INNER_ERROR_DESCRIPTION)).AsString());
				int responseCodeValue = sideProcessor.getResults(Constants.EDHOC_MESSAGE_1, false).
			            get(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_ERROR)).get(0).
			            get(Integer.valueOf(Constants.SIDE_PROCESSOR_INNER_ERROR_RESP_CODE)).AsInt32();
				responseCode = ResponseCode.valueOf(responseCodeValue);
				error = true;
				
				// No need to keep this information any longer in the side processor object
				sideProcessor.removeResultSet(Constants.EDHOC_MESSAGE_1, Constants.SIDE_PROCESSOR_OUTER_ERROR, false);
			}
		}
		
		// If EAD_1 was present, print any available results from processing its EAD items.
		sideProcessor.showResultsFromSideProcessing(Constants.EDHOC_MESSAGE_1, false);
		
		
		/* Return an EDHOC Error Message */
		
		if (error == true) {
			
			// Prepare SUITES_R
			suitesR = Util.buildSuitesR(cipherSuitesToOffer);
			
			byte[] connectionIdentifier = decodeIdentifier(cI);
			return processError(errorCode, Constants.EDHOC_MESSAGE_1, !isReq,
								connectionIdentifier, errMsg, suitesR, responseCode);
			
		}
		
		
		/* Return an indication to prepare EDHOC Message 2 */
		
		// A CBOR byte string with zero length, indicating that the EDHOC Message 2 can be prepared
		processingResult.add(CBORObject.FromObject(replyPayload));
		
		System.out.println("\nCompleted processing of EDHOC Message 1");
		return processingResult;
		
	}
	
    /**
     *  Process an EDHOC Message 2
     * @param sequence   The CBOR sequence used as payload of the EDHOC Message 2
     * @param isReq   True if the CoAP message is a request, or False otherwise
     * @param connectionIdInitiator   The already known connection identifier of the Initiator.
     * 								  It is set to null if expected in the EDHOC Message 2.
     * @param edhocSessions   The list of active EDHOC sessions of the recipient
     * @param peerPublicKeys   The list of the long-term public keys of authorized peers
     * @param peerCredentials   The list of CRED of the long-term public keys of authorized peers
     * @param usedConnectionIds   The set of already allocated Connection Identifiers
     * @param ownIdCreds   The set of ID_CRED_X used for an authentication credential associated to this peer
     * @return   A list of CBOR Objects including up to two elements.
     * 
     *           The first element is a CBOR byte string, with value either:
     *              i) a zero length byte string, indicating that the EDHOC message_2 was successfully processed; or
     *             ii) a non-zero length byte string as the EDHOC Error Message to be sent.
     *           
     *           In case (ii), the second element is a CBOR integer, with value the CoAP response code
     *           to use for the EDHOC Error Message, if this is a CoAP response.
     */
	public static List<CBORObject> readMessage2(byte[] sequence, boolean isReq, byte[] connectionIdInitiator,
												HashMap<CBORObject, EdhocSession> edhocSessions,
												HashMap<CBORObject, OneKey> peerPublicKeys,
												HashMap<CBORObject, CBORObject> peerCredentials,
												Set<CBORObject> usedConnectionIds,
			                                    Set<CBORObject> ownIdCreds) {
		
		if (sequence == null || edhocSessions == null ||
		    peerPublicKeys == null || peerCredentials == null || usedConnectionIds == null)
			return null;
		
		byte[] connectionIdentifierInitiator = null; // The connection identifier of the Initiator
		byte[] connectionIdentifierResponder = null; // The connection identifier of the Responder
		
		CBORObject[] ead2 = null; // Will be set if External Authorization Data is present as EAD2
		
		List<CBORObject> processingResult = new ArrayList<CBORObject>(); // List of CBOR Objects to return as result
		
		boolean error = false; // Will be set to True if an EDHOC Error Message has to be returned
		int errorCode = Constants.ERR_CODE_UNSPECIFIED_ERROR; // The error code to use for the EDHOC Error Message
		ResponseCode responseCode = null; // Will be set to the CoAP response code to use for the EDHOC Error Message
		String errMsg = null; // The text string to be possibly returned as DIAG_MSG in an EDHOC Error Message
		CBORObject cR = null; // The Connection Identifier C_R, or left to null in case of invalid message
		EdhocSession session = null; // The session used for this EDHOC execution
		
		
		int index = 0;
		CBORObject[] objectList = null;
		try {
			objectList = CBORObject.DecodeSequenceFromBytes(sequence);
		}
		catch (Exception e) {
		    errMsg = new String("Malformed or invalid EDHOC message_2");
		    responseCode = ResponseCode.BAD_REQUEST;
		    error = true;
		}
		
		
		/* Consistency checks */
				
		// If EDHOC Message 2 is transported in a CoAP request, C_I is present as first element of the CBOR sequence
		if (error == false && isReq == true) {
			if (error == false && objectList[index].getType() != CBORType.ByteString &&
				objectList[index].getType() != CBORType.Integer)  {
					errMsg = new String("C_I must be a byte string or an integer");
					responseCode = ResponseCode.BAD_REQUEST;
					error = true;
			}
			
			if (error == false) {
				CBORObject cI = objectList[index];
				connectionIdentifierInitiator = decodeIdentifier(cI);
				if (connectionIdentifierInitiator == null) {
				    errMsg = new String("Invalid encoding of C_I");
				    responseCode = ResponseCode.BAD_REQUEST;
				    error = true;
				}
				else {
					index++;
					if (debugPrint) {
					    Util.nicePrint("Connection Identifier of the Initiator", connectionIdentifierInitiator);
					}
				}
			}
			
		}
		
		if (error == false && isReq == false) {
			connectionIdentifierInitiator = connectionIdInitiator;
			if (debugPrint) {
			    Util.nicePrint("Connection Identifier of the Initiator", connectionIdentifierInitiator);
			}
		}
		
		if (error == false) {
				CBORObject connectionIdentifierInitiatorCbor = CBORObject.FromObject(connectionIdentifierInitiator);
				session = edhocSessions.get(connectionIdentifierInitiatorCbor);
			
			if (session == null) {
				errMsg = new String("EDHOC session not found");
				responseCode = ResponseCode.BAD_REQUEST;
				error = true;
			}
			else if (session.isInitiator() == false) {
				errMsg = new String("EDHOC Message 2 is intended only to an Initiator");
				responseCode = ResponseCode.BAD_REQUEST;
				error = true;
			}
			else if (session.getCurrentStep() != Constants.EDHOC_SENT_M1) {
				errMsg = new String("The protocol state is not waiting for an EDHOC Message 2");
				responseCode = ResponseCode.BAD_REQUEST;
				error = true;
			}
		}
		
		// G_Y | CIPHERTEXT_2
		byte[] gY = null;
		byte[] ciphertext2 = null;
		byte[] gY_Ciphertext2 = null;
		int gYLength = 0;
		int ciphetertext2Length = 0;
		if (error == false && objectList[index].getType() != CBORType.ByteString) {
				errMsg = new String("(G_Y | CIPHERTEXT_2) must be a byte string");
				responseCode = ResponseCode.BAD_REQUEST;
				error = true;
		}
		if (error == false) {
			gY_Ciphertext2 = objectList[index].GetByteString();
			
			gYLength = EdhocSession.getEphermeralKeyLength(session.getSelectedCipherSuite());
			ciphetertext2Length = gY_Ciphertext2.length - gYLength;
			
			if (ciphetertext2Length <= 0) {
				errMsg = new String("(G_Y | CIPHERTEXT_2) has an inconsistent size");
				responseCode = ResponseCode.BAD_REQUEST;
				error = true;
			}
				
		}
		if (error == false) {
			
			// G_Y
			gY = new byte[gYLength];
			System.arraycopy(gY_Ciphertext2, 0, gY, 0, gYLength);
	    	if (debugPrint) {
	    		Util.nicePrint("G_Y", gY);
	    	}
			
			// Set the ephemeral public key of the Responder
			OneKey peerEphemeralKey = null;
			
			int selectedCipherSuite = session.getSelectedCipherSuite();
			
			if (selectedCipherSuite == Constants.EDHOC_CIPHER_SUITE_0 ||
				selectedCipherSuite == Constants.EDHOC_CIPHER_SUITE_1) {
				peerEphemeralKey = SharedSecretCalculation.buildCurve25519OneKey(null, gY);
			}
			if (selectedCipherSuite == Constants.EDHOC_CIPHER_SUITE_2 ||
				selectedCipherSuite == Constants.EDHOC_CIPHER_SUITE_3) {
				peerEphemeralKey = SharedSecretCalculation.buildEcdsa256OneKey(null, gY, null);
			}
			
			if (peerEphemeralKey == null) {
				errMsg = new String("Invalid ephemeral public key G_Y");
				responseCode = ResponseCode.BAD_REQUEST;
				error = true;
			}
			else {
				session.setPeerEphemeralPublicKey(peerEphemeralKey);
		    	if (debugPrint) {
		    		Util.nicePrint("PeerEphemeralKey", peerEphemeralKey.AsCBOR().EncodeToBytes());
		    	}
			}
			
			
			// CIPHERTEXT_2
			ciphertext2 = new byte[ciphetertext2Length];
			System.arraycopy(gY_Ciphertext2, gYLength, ciphertext2, 0, ciphetertext2Length);

		}
		
		/* Return an EDHOC Error Message */
		
		if (error == true) {
			Util.purgeSession(session, connectionIdentifierInitiator, edhocSessions, usedConnectionIds);
			return processError(errorCode, Constants.EDHOC_MESSAGE_2, !isReq, connectionIdentifierResponder,
								errMsg, null, responseCode);
		}
		
		
		/* Decrypt CIPHERTEXT_2 */
			
        // Compute TH2
		
        byte[] th2 = null;
        byte[] hashMessage1 = session.getHashMessage1(); // the hash of message_1, as plain bytes
        List<CBORObject> objectListData2 = new ArrayList<>();
        for (int i = 0; i < objectList.length - 1; i++)
        	objectListData2.add(objectList[i]);
        byte[] hashMessage1SerializedCBOR = CBORObject.FromObject(hashMessage1).EncodeToBytes();
        byte[] gYSerializedCBOR = CBORObject.FromObject(gY).EncodeToBytes();
        
        th2 = computeTH2(session, gYSerializedCBOR, hashMessage1SerializedCBOR);
        if (th2 == null) {
        	errMsg = new String("Error when computing TH2");
        	responseCode = ResponseCode.INTERNAL_SERVER_ERROR;
        	Util.purgeSession(session, connectionIdentifierInitiator, edhocSessions, usedConnectionIds);
			return processError(errorCode, Constants.EDHOC_MESSAGE_2, !isReq, connectionIdentifierResponder,
								errMsg, null, responseCode);
        }
        else if (debugPrint) {
        	Util.nicePrint("H(message_1)", hashMessage1);
    		Util.nicePrint("TH_2", th2);
    	}
        session.setTH2(th2);
        session.cleanMessage1();
        
        
        // Compute the key material
		
        byte[] prk2e = null;
        byte[] prk3e2m = null;
        
        // Compute the Diffie-Hellman secret G_XY
        byte[] dhSecret = SharedSecretCalculation.generateSharedSecret(session.getEphemeralKey(),
        															   session.getPeerEphemeralPublicKey());
    	if (dhSecret == null) {
        	errMsg = new String("Error when computing the Diffie-Hellman secret G_XY");
        	responseCode = ResponseCode.INTERNAL_SERVER_ERROR;
        	Util.purgeSession(session, connectionIdentifierInitiator, edhocSessions, usedConnectionIds);
			return processError(errorCode, Constants.EDHOC_MESSAGE_2, !isReq, connectionIdentifierResponder,
								errMsg, null, responseCode);
    	}
    	else if (debugPrint) {
    		Util.nicePrint("G_XY", dhSecret);
    	}
        
        // Compute PRK_2e
    	String hashAlgorithm = EdhocSession.getEdhocHashAlg(session.getSelectedCipherSuite());
    	
    	prk2e = computePRK2e(th2, dhSecret, hashAlgorithm);    	
    	
    	dhSecret = null;
    	if (prk2e == null) {
        	errMsg = new String("Error when computing PRK_2e");
        	responseCode = ResponseCode.INTERNAL_SERVER_ERROR;
        	Util.purgeSession(session, connectionIdentifierInitiator, edhocSessions, usedConnectionIds);
			return processError(errorCode, Constants.EDHOC_MESSAGE_2, !isReq, connectionIdentifierResponder,
								errMsg, null, responseCode);
    	}
    	else if (debugPrint) {
    		Util.nicePrint("PRK_2e", prk2e);
    	}
    	session.setPRK2e(prk2e);
    	
    	// Compute KEYSTREAM_2
    	byte[] keystream2 = computeKeystream2(session, ciphertext2.length);
    	if (keystream2 == null) {
        	errMsg = new String("Error when computing KEYSTREAM_2");
        	responseCode = ResponseCode.INTERNAL_SERVER_ERROR;
        	Util.purgeSession(session, connectionIdentifierInitiator, edhocSessions, usedConnectionIds);
			return processError(errorCode, Constants.EDHOC_MESSAGE_2, !isReq, connectionIdentifierResponder,
								errMsg, null, responseCode);
    	}
    	else if (debugPrint) {
    		Util.nicePrint("KEYSTREAM_2", keystream2);
    	}
		
    	// Compute the plaintext
    	
    	if (debugPrint && ciphertext2 != null) {
    		Util.nicePrint("CIPHERTEXT_2", ciphertext2);
    	}
    	byte[] plaintext2 = Util.arrayXor(ciphertext2, keystream2);
    	if (debugPrint && plaintext2 != null) {
    		Util.nicePrint("Plaintext retrieved from CIPHERTEXT_2", plaintext2);
    	}
    	
    	error = false;
    	
    	// Parse the plaintext as a CBOR sequence
    	CBORObject[] plaintextElementList = null;
    	CBORObject idCredR = CBORObject.NewMap();
    	try {
    		plaintextElementList = CBORObject.DecodeSequenceFromBytes(plaintext2);
    	}
    	catch (Exception e) {
    	    errMsg = new String("Malformed or invalid plaintext from CIPHERTEXT_2");
    	    responseCode = ResponseCode.BAD_REQUEST;
    	    error = true;
    	}
    	if (error == false && plaintextElementList.length < 3) {
        	errMsg = new String("Invalid format of the content encrypted as CIPHERTEXT_2");
        	responseCode = ResponseCode.BAD_REQUEST;
        	error = true;
    	}
    	if (error == false) {
    	    cR = plaintextElementList[0];

    	    if (cR.getType() != CBORType.ByteString && cR.getType() != CBORType.Integer) {
    	            errMsg = new String("C_R must be a byte string or an integer");
    	            responseCode = ResponseCode.BAD_REQUEST;
    	            error = true;
    	    }
    	    if (error == false) {
    	        connectionIdentifierResponder = decodeIdentifier(cR);
    	        if (connectionIdentifierResponder == null) {
    	            errMsg = new String("Invalid encoding of C_R");
    	            responseCode = ResponseCode.BAD_REQUEST;
    	            error = true;
    	        }
    	        else {
    	            session.setPeerConnectionId(connectionIdentifierResponder);
    	            if (debugPrint) {
    	                Util.nicePrint("Connection Identifier of the Responder", connectionIdentifierResponder);
    	                Util.nicePrint("C_R", cR.EncodeToBytes());
    	            }
    	        }
    	    }
    	    if (error == false) {
    	        if (session.getApplicationProfile().getUsedForOSCORE() == true &&
    	            Arrays.equals(connectionIdentifierInitiator, connectionIdentifierResponder) == true) {
    	            errMsg = new String("C_R must be different from C_I");
    	            responseCode = ResponseCode.BAD_REQUEST;
    	            error = true;
    	        }
    	    }

    	}
    	if (error == false &&
    			 plaintextElementList[1].getType() != CBORType.ByteString &&
    			 plaintextElementList[1].getType() != CBORType.Integer &&
    			 plaintextElementList[1].getType() != CBORType.Map) {
        	errMsg = new String("Invalid format of ID_CRED_R");
        	responseCode = ResponseCode.BAD_REQUEST;
        	error = true;
    	}
    	if (error == false) {
        	CBORObject rawIdCredR = plaintextElementList[1];
        	
        	// ID_CRED_R is a CBOR map with 'kid', and only 'kid' was transported
        	if (rawIdCredR.getType() == CBORType.ByteString || rawIdCredR.getType() == CBORType.Integer) {
        		byte[] kidValue = MessageProcessor.decodeIdentifier(rawIdCredR);
        		idCredR.Add(HeaderKeys.KID.AsCBOR(), kidValue);
        	}
        	else if (rawIdCredR.getType() == CBORType.Map) {
        		idCredR = rawIdCredR;
        	}
        	else {
        	    errMsg = new String("Invalid format for ID_CRED_R");
        	    responseCode = ResponseCode.BAD_REQUEST;
        	    error = true;
        	}
    	}
    	if (error == false && ownIdCreds.contains(idCredR)) {
        	errMsg = new String("The identity expressed by ID_CRED_R is equal to my own identity");
        	responseCode = ResponseCode.BAD_REQUEST;
			error = true;
    	}
    	if (error == false && plaintextElementList[2].getType() != CBORType.ByteString) {
        	errMsg = new String("Signature_or_MAC_2 must be a byte string");
        	responseCode = ResponseCode.BAD_REQUEST;
        	error = true;
    	}
    	if (error == false && plaintextElementList.length > 3) {
    		// EAD_2 is present
		    ead2 = preParseEAD(plaintextElementList, 3, 2, session.getSupportedEADs());
		   
		    if (ead2.length == 1 && ead2[0].getType() == CBORType.TextString) {
		    	errMsg = new String(ead2[0].toString());
		        responseCode = ResponseCode.BAD_REQUEST;
		        ead2 = null;
		        error = true;
		    }
    	}
    	
    	if (error == true) {
    		Util.purgeSession(session, connectionIdentifierInitiator, edhocSessions, usedConnectionIds);
			return processError(errorCode, Constants.EDHOC_MESSAGE_2, !isReq, connectionIdentifierResponder,
								errMsg, null, responseCode);
    	}
    	
    	error = false;
    	
    	
    	CBORObject peerCredentialCBOR = null;
    	
    	// Invoke the retrieval and/or validation of CRED_R, and the processing of EAD items in EAD_2 (if any)
    	SideProcessor sideProcessor = session.getSideProcessor();
 	    CBORObject[] sideProcessorInfo = new CBORObject[3];
 	    sideProcessorInfo[0] = CBORObject.FromObject(gY);
 	    sideProcessorInfo[1] = CBORObject.FromObject(connectionIdentifierResponder);
 	    sideProcessorInfo[2] = CBORObject.FromObject(idCredR);
	   
	    sideProcessor.sideProcessingMessage2PreVerification(sideProcessorInfo, ead2);
	   
	    // A fatal error occurred
	    if (sideProcessor.getResults(Constants.EDHOC_MESSAGE_2, false).
	    		containsKey(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_ERROR))) {
	    	errMsg = new String(sideProcessor.getResults(Constants.EDHOC_MESSAGE_2, false).
	        	get(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_ERROR)).get(0).
	            get(Integer.valueOf(Constants.SIDE_PROCESSOR_INNER_ERROR_DESCRIPTION)).AsString());
	        int responseCodeValue = sideProcessor.getResults(Constants.EDHOC_MESSAGE_2, false).
	        	get(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_ERROR)).get(0).
	            get(Integer.valueOf(Constants.SIDE_PROCESSOR_INNER_ERROR_RESP_CODE)).AsInt32();
	        responseCode = ResponseCode.valueOf(responseCodeValue);
	        error = true;
	         
	        // No need to keep this information any longer in the side processor object
	        sideProcessor.removeResultSet(Constants.EDHOC_MESSAGE_2, Constants.SIDE_PROCESSOR_OUTER_ERROR, false);
	   }

    	// If no fatal error occurred, the side processor object includes the authentication credential
    	// of the other peer, if a valid one was found during the side processing above.
 	   	if (error == false && sideProcessor.getResults(Constants.EDHOC_MESSAGE_2, false).
 	   						  containsKey(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_CRED))) {
 	   		peerCredentialCBOR = sideProcessor.getResults(Constants.EDHOC_MESSAGE_2, false).
 	   							 get(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_CRED)).get(0).
 	   							 get(Integer.valueOf(Constants.SIDE_PROCESSOR_INNER_CRED_VALUE));

 	    	if (peerCredentialCBOR == null) {
 	        	errMsg = new String("Unable to retrieve the peer credential");
 	        	responseCode = ResponseCode.BAD_REQUEST;
 	        	error = true;
 	    	}
	    }
	     	   	
        // No need to keep this information any longer in the side processor object
        sideProcessor.removeResultSet(Constants.EDHOC_MESSAGE_2, Constants.SIDE_PROCESSOR_OUTER_CRED, false);
        
        // If EAD_2 was present, print any available results from processing its EAD items.
        sideProcessor.showResultsFromSideProcessing(Constants.EDHOC_MESSAGE_2, false);
    	
    	if (error == true) {
    		Util.purgeSession(session, connectionIdentifierInitiator, edhocSessions, usedConnectionIds);
			return processError(errorCode, Constants.EDHOC_MESSAGE_2, !isReq, connectionIdentifierResponder,
								errMsg, null, responseCode);
    	}
    
    	
    	OneKey peerKey = peerPublicKeys.get(idCredR);
    	session.setPeerLongTermPublicKey(peerKey);
    	
    	
    	// Compute PRK_3e2m
    	prk3e2m = computePRK3e2m(session, prk2e);
    	if (prk3e2m == null) {
        	errMsg = new String("Error when computing PRK_3e2m");
        	responseCode = ResponseCode.INTERNAL_SERVER_ERROR;
        	Util.purgeSession(session, connectionIdentifierInitiator, edhocSessions, usedConnectionIds);
			return processError(errorCode, Constants.EDHOC_MESSAGE_2, !isReq, connectionIdentifierResponder,
								errMsg, null, responseCode);
    	}
    	else if (debugPrint) {
	    		Util.nicePrint("PRK_3e2m", prk3e2m);
    	}
    	session.setPRK3e2m(prk3e2m);
    	
    	
    	/* Start verifying Signature_or_MAC_2 */
    	
    	byte[] peerCredential = peerCredentialCBOR.GetByteString(); // CRED_R
    	    	
    	session.setPeerIdCred(idCredR);      // Store ID_CRED_R
    	session.setPeerCred(peerCredential); // Store CRED_R
    	
    	// Compute MAC_2
    	byte[] mac2 = computeMAC2(session, prk3e2m, th2, idCredR, peerCredential, ead2);
    	if (mac2 == null) {
        	errMsg = new String("Error when computing MAC_2");
        	responseCode = ResponseCode.INTERNAL_SERVER_ERROR;
        	Util.purgeSession(session, connectionIdentifierInitiator, edhocSessions, usedConnectionIds);
			return processError(errorCode, Constants.EDHOC_MESSAGE_2, !isReq, connectionIdentifierResponder,
								errMsg, null, responseCode);
    	}
    	else if (debugPrint) {
    		Util.nicePrint("MAC_2", mac2);
    	}
            	
    	// Verify Signature_or_MAC_2
    	byte[] signatureOrMac2 = plaintextElementList[2].GetByteString();
    	if (debugPrint && signatureOrMac2 != null) {
    		Util.nicePrint("Signature_or_MAC_2", signatureOrMac2);
    	}
        
    	// Prepare the External Data, as a CBOR sequence
    	byte[] externalData = computeExternalData(th2, peerCredential, ead2);
    	if (externalData == null) {
        	errMsg = new String("Error when computing External Data for MAC_2");
        	responseCode = ResponseCode.INTERNAL_SERVER_ERROR;
        	Util.purgeSession(session, connectionIdentifierInitiator, edhocSessions, usedConnectionIds);
			return processError(errorCode, Constants.EDHOC_MESSAGE_2, !isReq, connectionIdentifierResponder,
								errMsg, null, responseCode);
    	}
    	else if (debugPrint) {
    		Util.nicePrint("External Data to verify Signature_or_MAC_2", externalData);
    	}
    	
    	if (!verifySignatureOrMac2(session, signatureOrMac2, externalData, mac2)) {
        	errMsg = new String("Non valid Signature_or_MAC_2");
        	responseCode = ResponseCode.BAD_REQUEST;
        	Util.purgeSession(session, connectionIdentifierInitiator, edhocSessions, usedConnectionIds);
			return processError(errorCode, Constants.EDHOC_MESSAGE_2, !isReq, connectionIdentifierResponder,
								errMsg, null, responseCode);
    	}
    	
    	/* End verifying Signature_or_MAC_2 */

    	// Invoke the processing of EAD items in EAD_2 (if any)
    	// that had to wait for a successful verification of Signature_or_MAC_2
    	if (ead2!= null && ead2.length != 0) {
    		sideProcessor.sideProcessingMessage2PostVerification(sideProcessorInfo, ead2);
    	}

    	// A fatal error occurred
    	if (sideProcessor.getResults(Constants.EDHOC_MESSAGE_2, true).
    			containsKey(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_ERROR))) {
    	   errMsg = new String(sideProcessor.getResults(Constants.EDHOC_MESSAGE_2, true).
    	         get(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_ERROR)).get(0).
    	         get(Integer.valueOf(Constants.SIDE_PROCESSOR_INNER_ERROR_DESCRIPTION)).AsString());
    	   int responseCodeValue = sideProcessor.getResults(Constants.EDHOC_MESSAGE_2, true).
    	         get(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_ERROR)).get(0).
    	         get(Integer.valueOf(Constants.SIDE_PROCESSOR_INNER_ERROR_RESP_CODE)).AsInt32();
    	   responseCode = ResponseCode.valueOf(responseCodeValue);
    	   error = true;
    	      
    	   // No need to keep this information any longer in the side processor object
    	   sideProcessor.removeResultSet(Constants.EDHOC_MESSAGE_2, Constants.SIDE_PROCESSOR_OUTER_ERROR, true);
    	}
    	
        // If EAD_2 was present, print any available results from processing its EAD items.
        sideProcessor.showResultsFromSideProcessing(Constants.EDHOC_MESSAGE_2, true);
    	
    	if (error == true) {
    		Util.purgeSession(session, connectionIdentifierInitiator, edhocSessions, usedConnectionIds);
			return processError(errorCode, Constants.EDHOC_MESSAGE_2, !isReq, connectionIdentifierResponder,
								errMsg, null, responseCode);
    	}
    	

    	// Store PLAINTEXT_2 for the later computation of TH_3
		session.setPlaintext2(plaintext2);
    	
		/* Return an indication to prepare EDHOC Message 3, possibly with the provided External Authorization Data */
		
		// A CBOR byte string with zero length, indicating that the EDHOC Message 3 can be prepared
		byte[] reply = new byte[] {};
		processingResult.add(CBORObject.FromObject(reply));
		
		System.out.println("\nCompleted processing of EDHOC Message 2");
		return processingResult;
		
	}
	
    /**
     *  Process an EDHOC Message 3
     * @param sequence   The CBOR sequence used as payload of the EDHOC Message 3
     * @param isReq   True if the CoAP message is a request, or False otherwise
     * @param connectionIdResponder   The already known connection identifier of the Responder.
     * 								  It is set to null if expected in the EDHOC Message 3.
     * @param edhocSessions   The list of active EDHOC sessions of the recipient
     * @param peerPublicKeys   The list of the long-term public keys of authorized peers
     * @param peerCredentials   The list of CRED of the long-term public keys of authorized peers
     * @param usedConnectionIds   The set of already allocated Connection Identifiers
     * @return   A list of CBOR Objects including two elements.
     * 
     *           The first element is a CBOR byte string, with value either:
     *              i) a zero length byte string, indicating that the EDHOC message_3 was successfully processed; or
     *             ii) a non-zero length byte string as the EDHOC Error Message to be sent.
     *           
     *           In case (i), the second element is a CBOR byte string, specifying the Connection Identifier
     *           of the Responder in the used EDHOC session, i.e., C_R.
     *           
     *           In case (ii), the second element is a CBOR integer, with value the CoAP response code
     *           to use for the EDHOC Error Message, if this is a CoAP response.
     */
	public static List<CBORObject> readMessage3(byte[] sequence, boolean isReq, byte[] connectionIdResponder,
												HashMap<CBORObject, EdhocSession> edhocSessions,
												HashMap<CBORObject, OneKey> peerPublicKeys,
												HashMap<CBORObject, CBORObject> peerCredentials,
												Set<CBORObject> usedConnectionIds) {
		
		if (sequence == null || edhocSessions == null ||
			peerPublicKeys == null || peerCredentials == null || usedConnectionIds == null)
			return null;
		
		byte[] connectionIdentifierInitiator = null; // The Connection Identifier of the Initiator
		byte[] connectionIdentifierResponder = null; // The Connection Identifier of the Responder
		
		CBORObject[] ead3 = null; // Will be set if External Authorization Data is present as EAD3
		
		List<CBORObject> processingResult = new ArrayList<CBORObject>(); // List of CBOR Objects to return as result
		
		boolean error = false; // Will be set to True if an EDHOC Error Message has to be returned
		int errorCode = Constants.ERR_CODE_UNSPECIFIED_ERROR; // The error code to use for the EDHOC Error Message
		ResponseCode responseCode = null; // Will be set to the CoAP response code to use for the EDHOC Error Message
		String errMsg = null; // The text string to be possibly returned as DIAG_MSG in an EDHOC Error Message
		EdhocSession session = null; // The session used for this EDHOC execution
		

		int index = 0;
		CBORObject[] objectList = null;
		try {
			objectList = CBORObject.DecodeSequenceFromBytes(sequence);
		}
		catch (Exception e) {
		    errMsg = new String("Malformed or invalid EDHOC message_3");
		    responseCode = ResponseCode.BAD_REQUEST;
		    error = true;
		}
		
		
		/* Consistency checks */
		
		// If EDHOC Message 3 is transported in a CoAP request, C_R is present as first element of the CBOR sequence
		if (error == false && isReq == true) {
			if (error == false && objectList[index].getType() != CBORType.ByteString &&
				objectList[index].getType() != CBORType.Integer)  {
					errMsg = new String("C_R must be a byte string or an integer");
					responseCode = ResponseCode.BAD_REQUEST;
					error = true;
			}
		    if (error == false) {
		        connectionIdentifierResponder = decodeIdentifier(objectList[index]);
		        if (connectionIdentifierResponder == null) {
		            errMsg = new String("Invalid encoding of C_R");
		            responseCode = ResponseCode.BAD_REQUEST;
		            error = true;
		        }
		        else {
		            index++;
					if (debugPrint) {
					    Util.nicePrint("Connection Identifier of the Responder", connectionIdentifierResponder);
					}
		        }
		    }

		}
		
		if (error == false && isReq == false) {
			connectionIdentifierResponder = connectionIdResponder;
			if (debugPrint) {
			    Util.nicePrint("Connection Identifier of the Responder", connectionIdentifierResponder);
			}
		}
			
		if (error == false) {
			CBORObject connectionIdentifierResponderCbor = CBORObject.FromObject(connectionIdentifierResponder);
			session = edhocSessions.get(connectionIdentifierResponderCbor);
			
			if (session == null) {
				errMsg = new String("EDHOC session not found");
				responseCode = ResponseCode.BAD_REQUEST;
				error = true;
			}
			else if (session.isInitiator() == true) {
				errMsg = new String("EDHOC Message 3 is intended only to a Responder");
				responseCode = ResponseCode.BAD_REQUEST;
				error = true;
			}
			else if (session.getCurrentStep() != Constants.EDHOC_SENT_M2) {
				errMsg = new String("The protocol state is not waiting for an EDHOC Message 3");
				responseCode = ResponseCode.BAD_REQUEST;
				error = true;
			}
		}
		
		if (session != null) {
			if (session.getPeerConnectionId() != null)
				connectionIdentifierInitiator = session.getPeerConnectionId();
		}
		
		
		// CIPHERTEXT_3
		byte[] ciphertext3 = null;
		if (error == false && objectList[index].getType() != CBORType.ByteString) {
			errMsg = new String("CIPHERTEXT_3 must be a byte string");
			responseCode = ResponseCode.BAD_REQUEST;
			error = true;
		}
		else {
			ciphertext3 = objectList[index].GetByteString();
		}
		
		
		/* Send an EDHOC Error Message */
		
		if (error == true) {
			Util.purgeSession(session, connectionIdentifierResponder, edhocSessions, usedConnectionIds);
			return processError(errorCode, Constants.EDHOC_MESSAGE_3, !isReq, connectionIdentifierInitiator,
								errMsg, null, responseCode);
		}
		
		
		/* Decrypt CIPHERTEXT_3 */
		
        // Compute TH3
        byte[] th2 = session.getTH2(); // TH_2 as plain bytes
        byte[] th2SerializedCBOR = CBORObject.FromObject(th2).EncodeToBytes();

        byte[] plaintext2 = session.getPlaintext2(); // PLAINTEXT_2 as serialized CBOR sequence
        
        byte[] credR = session.getCred();
        byte[] th3 = computeTH3(session, th2SerializedCBOR, plaintext2, credR);
        
        if (th3 == null) {
        	errMsg = new String("Error when computing TH3");
        	responseCode = ResponseCode.INTERNAL_SERVER_ERROR;
        	Util.purgeSession(session, connectionIdentifierResponder, edhocSessions, usedConnectionIds);
			return processError(errorCode, Constants.EDHOC_MESSAGE_3, !isReq, connectionIdentifierInitiator,
								errMsg, null, responseCode);
        }
        else if (debugPrint) {
    		Util.nicePrint("TH_3", th3);
    	}
        session.setTH3(th3);
		
		
    	// Compute K_3 and IV_3 to protect the outer COSE object
    	byte[] k3 = computeKey(Constants.EDHOC_K_3, session);
    	if (k3 == null) {
        	errMsg = new String("Error when computing K_3");
        	responseCode = ResponseCode.INTERNAL_SERVER_ERROR;
        	Util.purgeSession(session, connectionIdentifierResponder, edhocSessions, usedConnectionIds);
			return processError(errorCode, Constants.EDHOC_MESSAGE_3, !isReq, connectionIdentifierInitiator,
								errMsg, null, responseCode);
    	}
    	else if (debugPrint) {
    		Util.nicePrint("K_3", k3);
    	}
    	
    	byte[] iv3 = computeIV(Constants.EDHOC_IV_3, session);
    	if (iv3 == null) {
        	errMsg = new String("Error when computing IV_3");
        	responseCode = ResponseCode.INTERNAL_SERVER_ERROR;
        	Util.purgeSession(session, connectionIdentifierResponder, edhocSessions, usedConnectionIds);
			return processError(errorCode, Constants.EDHOC_MESSAGE_3, !isReq, connectionIdentifierInitiator,
								errMsg, null, responseCode);
    	}
    	else if (debugPrint) {
    		Util.nicePrint("IV_3", iv3);
    	}
    	
    	// Prepare the external_aad as including only TH3
    	byte[] externalData = th3;

    	
    	// Compute the plaintext
    	
    	if (debugPrint && ciphertext3 != null) {
    		Util.nicePrint("CIPHERTEXT_3", ciphertext3);
    	}

    	byte[] plaintext3 = decryptCiphertext3(session, externalData, ciphertext3, k3, iv3);
    	if (plaintext3 == null) {
        	errMsg = new String("Error when decrypting CIPHERTEXT_3");
        	responseCode = ResponseCode.INTERNAL_SERVER_ERROR;
        	Util.purgeSession(session, connectionIdentifierResponder, edhocSessions, usedConnectionIds);
			return processError(errorCode, Constants.EDHOC_MESSAGE_3, !isReq, connectionIdentifierInitiator,
								errMsg, null, responseCode);
    	}
    	else if (debugPrint) {
    		Util.nicePrint("Plaintext retrieved from CIPHERTEXT_3", plaintext3);
    	}
    	
    	error = false;
    	
    	// Parse the outer plaintext as a CBOR sequence
    	CBORObject[] plaintextElementList = null;
    	CBORObject idCredI = CBORObject.NewMap();
    	try {
    		plaintextElementList = CBORObject.DecodeSequenceFromBytes(plaintext3);
    	}
    	catch (Exception e) {
    	    errMsg = new String("Malformed or invalid plaintext from CIPHERTEXT_3");
    	    responseCode = ResponseCode.BAD_REQUEST;
    	    error = true;
    	}
    	if (error == false && plaintextElementList.length < 2) {
    	    errMsg = new String("Invalid format of the content encrypted as CIPHERTEXT_3");
    	    responseCode = ResponseCode.BAD_REQUEST;
    	    error = true;
    	}
    	if (error == false &&
                plaintextElementList[0].getType() != CBORType.ByteString &&
                plaintextElementList[0].getType() != CBORType.Integer &&
                plaintextElementList[0].getType() != CBORType.Map) {
	        errMsg = new String("Invalid format of ID_CRED_I");
	        responseCode = ResponseCode.BAD_REQUEST;
	        error = true;
    	}
    	if (error == false) {
        	CBORObject rawIdCredI = plaintextElementList[0];
        	
        	// ID_CRED_I is a CBOR map with 'kid', and only 'kid' was transported
        	if (rawIdCredI.getType() == CBORType.ByteString || rawIdCredI.getType() == CBORType.Integer) {
        	    byte[] kidValue = MessageProcessor.decodeIdentifier(rawIdCredI);
        	    idCredI.Add(HeaderKeys.KID.AsCBOR(), kidValue);
        	}
        	else if (rawIdCredI.getType() == CBORType.Map) {
        	    idCredI = rawIdCredI;
        	}
        	else {
        	    errMsg = new String("Invalid format for ID_CRED_I");
        	    responseCode = ResponseCode.BAD_REQUEST;
        	    error = true;
        	}
    	}    	
    	if (error == false && plaintextElementList[1].getType() != CBORType.ByteString) {
    	    errMsg = new String("Signature_or_MAC_3 must be a byte string");
    	    responseCode = ResponseCode.BAD_REQUEST;
    	    error = true;
    	}
    	if (error == false && plaintextElementList.length > 2) {
		   // EAD_3 is present
		   ead3 = preParseEAD(plaintextElementList, 2, 3, session.getSupportedEADs());
		   
		   if (ead3.length == 1 && ead3[0].getType() == CBORType.TextString) {
		         errMsg = new String(ead3[0].toString());
		         responseCode = ResponseCode.BAD_REQUEST;
			     ead3 = null;
		         error = true;
		   }
    	}
    	
    	if (error == true) {
    		Util.purgeSession(session, connectionIdentifierResponder, edhocSessions, usedConnectionIds);
			return processError(errorCode, Constants.EDHOC_MESSAGE_3, !isReq, connectionIdentifierInitiator,
								errMsg, null, responseCode);
    	}
    	
    	error = false;
    	
    	CBORObject peerCredentialCBOR = null;

    	// Invoke the retrieval and/or validation of CRED_I, and the processing of EAD items in EAD_3 (if any)
    	SideProcessor sideProcessor = session.getSideProcessor();
 	    CBORObject[] sideProcessorInfo = new CBORObject[1];
 	    sideProcessorInfo[0] = CBORObject.FromObject(idCredI);
    	
 	    sideProcessor.sideProcessingMessage3PreVerification(sideProcessorInfo, ead3);

	    // A fatal error occurred
	    if (sideProcessor.getResults(Constants.EDHOC_MESSAGE_3, false).
	    		containsKey(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_ERROR))) {
	    	errMsg = new String(sideProcessor.getResults(Constants.EDHOC_MESSAGE_3, false).
	        	get(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_ERROR)).get(0).
	            get(Integer.valueOf(Constants.SIDE_PROCESSOR_INNER_ERROR_DESCRIPTION)).AsString());
	        int responseCodeValue = sideProcessor.getResults(Constants.EDHOC_MESSAGE_3, false).
	        	get(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_ERROR)).get(0).
	            get(Integer.valueOf(Constants.SIDE_PROCESSOR_INNER_ERROR_RESP_CODE)).AsInt32();
	        responseCode = ResponseCode.valueOf(responseCodeValue);
	        error = true;

	        // No need to keep this information any longer in the side processor object
	        sideProcessor.removeResultSet(Constants.EDHOC_MESSAGE_3, Constants.SIDE_PROCESSOR_OUTER_ERROR, false);
	    }
	        	
    	// If no fatal error occurred, the side processor object includes the authentication credential
    	// of the other peer, if a valid one was found during the side processing above.
 	   	if (error == false && sideProcessor.getResults(Constants.EDHOC_MESSAGE_3, false).
 	   						  containsKey(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_CRED))) {
 	   		peerCredentialCBOR = sideProcessor.getResults(Constants.EDHOC_MESSAGE_3, false).
 	   							 get(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_CRED)).get(0).
 	   							 get(Integer.valueOf(Constants.SIDE_PROCESSOR_INNER_CRED_VALUE));

 	    	if (peerCredentialCBOR == null) {
 	        	errMsg = new String("Unable to retrieve the peer credential");
 	        	responseCode = ResponseCode.BAD_REQUEST;
 	        	error = true;
 	    	}
	    }
    	
        // No need to keep this information any longer in the side processor object
        sideProcessor.removeResultSet(Constants.EDHOC_MESSAGE_3, Constants.SIDE_PROCESSOR_OUTER_CRED, false);
        
        // If EAD_3 was present, print any available results from processing its EAD items.
        sideProcessor.showResultsFromSideProcessing(Constants.EDHOC_MESSAGE_3, false);
    	
    	if (error == true) {
    		Util.purgeSession(session, connectionIdentifierResponder, edhocSessions, usedConnectionIds);
			return processError(errorCode, Constants.EDHOC_MESSAGE_3, !isReq, connectionIdentifierInitiator, errMsg, null, responseCode);
    	}
    	
    	
    	OneKey peerKey = peerPublicKeys.get(idCredI);
    	session.setPeerLongTermPublicKey(peerKey);
    	
    	
        // Compute PRK_4e3m
        byte[] prk4e3m = computePRK4e3m(session);
    	if (prk4e3m == null) {
    		errMsg = new String("Error when computing PRK_4e3m");
    		responseCode = ResponseCode.INTERNAL_SERVER_ERROR;
    		Util.purgeSession(session, connectionIdentifierResponder, edhocSessions, usedConnectionIds);
			return processError(errorCode, Constants.EDHOC_MESSAGE_3, !isReq, connectionIdentifierInitiator,
								errMsg, null, responseCode);
    	}
    	else if (debugPrint) {
    		Util.nicePrint("PRK_4e3m", prk4e3m);
    	}
    	session.setPRK4e3m(prk4e3m);

    	
    	/* Start verifying Signature_or_MAC_3 */

    	byte[] peerCredential = peerCredentialCBOR.GetByteString(); // CRED_I
    	    	
    	session.setPeerIdCred(idCredI); // Store ID_CRED_I
    	
    	// Compute MAC_3
    	byte[] mac3 = computeMAC3(session, prk4e3m, th3, idCredI, peerCredential, ead3);
    	if (mac3 == null) {
    		errMsg = new String("Error when computing MAC_3");
    		responseCode = ResponseCode.INTERNAL_SERVER_ERROR;
    		Util.purgeSession(session, connectionIdentifierResponder, edhocSessions, usedConnectionIds);
			return processError(errorCode, Constants.EDHOC_MESSAGE_3, !isReq, connectionIdentifierInitiator,
								errMsg, null, responseCode);
    	}
    	else if (debugPrint) {
    		Util.nicePrint("MAC_3", mac3);
    	}
    	
    	
    	// Verify Signature_or_MAC_3
    	
    	byte[] signatureOrMac3 = plaintextElementList[1].GetByteString();
    	if (debugPrint && signatureOrMac3 != null) {
    		Util.nicePrint("Signature_or_MAC_3", signatureOrMac3);
    	}
    	
        // Prepare the external data, as a CBOR sequence
    	externalData = computeExternalData(th3, peerCredential, ead3);
    	if (externalData == null) {
    		errMsg = new String("Error when computing the external data for MAC_3");
    		responseCode = ResponseCode.INTERNAL_SERVER_ERROR;
    		Util.purgeSession(session, connectionIdentifierResponder, edhocSessions, usedConnectionIds);
    		return processError(errorCode, Constants.EDHOC_MESSAGE_3, !isReq, connectionIdentifierInitiator,
    							errMsg, null, responseCode);
    	}
    	else if (debugPrint) {
    		Util.nicePrint("External Data to verify Signature_or_MAC_3", externalData);
    	}
    	
    	if (!verifySignatureOrMac3(session, signatureOrMac3, externalData, mac3)) {
        	errMsg = new String("Non valid Signature_or_MAC_3");
        	responseCode = ResponseCode.BAD_REQUEST;
        	Util.purgeSession(session, connectionIdentifierResponder, edhocSessions, usedConnectionIds);
			return processError(errorCode, Constants.EDHOC_MESSAGE_3, !isReq, connectionIdentifierInitiator,
								errMsg, null, responseCode);
    	}
    	
    	/* End verifying Signature_or_MAC_3 */
    	
    	// Invoke the processing of EAD items in EAD_3 (if any)
    	// that had to wait for a successful verification of Signature_or_MAC_3
    	if (ead3!= null && ead3.length != 0) {
    		sideProcessor.sideProcessingMessage3PostVerification(sideProcessorInfo, ead3);
    	}

    	// A fatal error occurred
    	if (sideProcessor.getResults(Constants.EDHOC_MESSAGE_3, true).
    			containsKey(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_ERROR))) {
    	   errMsg = new String(sideProcessor.getResults(Constants.EDHOC_MESSAGE_3, true).
    	         get(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_ERROR)).get(0).
    	         get(Integer.valueOf(Constants.SIDE_PROCESSOR_INNER_ERROR_DESCRIPTION)).AsString());
    	   int responseCodeValue = sideProcessor.getResults(Constants.EDHOC_MESSAGE_3, true).
    	         get(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_ERROR)).get(0).
    	         get(Integer.valueOf(Constants.SIDE_PROCESSOR_INNER_ERROR_RESP_CODE)).AsInt32();
    	   responseCode = ResponseCode.valueOf(responseCodeValue);
    	   error = true;
    	      
    	   // No need to keep this information any longer in the side processor object
    	   sideProcessor.removeResultSet(Constants.EDHOC_MESSAGE_3, Constants.SIDE_PROCESSOR_OUTER_ERROR, true);
    	}
    	
        // If EAD_3 was present, print any available results from processing its EAD items.
        sideProcessor.showResultsFromSideProcessing(Constants.EDHOC_MESSAGE_3, true);
    	
    	if (error == true) {
    		Util.purgeSession(session, connectionIdentifierInitiator, edhocSessions, usedConnectionIds);
			return processError(errorCode, Constants.EDHOC_MESSAGE_3, !isReq, connectionIdentifierResponder,
								errMsg, null, responseCode);
    	}
    	
    	
    	
    	
    	/* Compute TH4 */
    	
        byte[] th3SerializedCBOR = CBORObject.FromObject(th3).EncodeToBytes();
        
    	byte[] th4 = computeTH4(session, th3SerializedCBOR, plaintext3, peerCredential);
    	
        if (th4 == null) {
        	errMsg = new String("Error when computing TH_4");
        	responseCode = ResponseCode.INTERNAL_SERVER_ERROR;
        	Util.purgeSession(session, connectionIdentifierResponder, edhocSessions, usedConnectionIds);
        	return processError(errorCode, Constants.EDHOC_MESSAGE_3, !isReq, connectionIdentifierInitiator,
        						errMsg, null, responseCode);
        }
        else if (debugPrint) {
    		Util.nicePrint("TH_4", th4);
    	}
        session.setTH4(th4);

    	/* Compute PRK_out */
    	byte[] prkOut = computePRKout(session, prk4e3m);
        if (prkOut == null) {
        	System.err.println("Error when computing PRK_out");
    		errMsg = new String("Error when computing PRK_out");
    		error = true;
        }
        else if (debugPrint) {
    		Util.nicePrint("PRK_out", prkOut);
    	}
    	session.setPRKout(prkOut);

    	/* Compute PRK_exporter */
    	byte[] prkExporter = computePRKexporter(session, prkOut);
        if (prkExporter == null) {
        	System.err.println("Error when computing PRK_exporter");
    		errMsg = new String("Error when computing PRK_exporter");
    		error = true;
        }
        else if (debugPrint) {
    		Util.nicePrint("PRK_exporter", prkExporter);
    	}
    	session.setPRKexporter(prkExporter);
        
    	
    	/* Delete ephemeral keys and other temporary material */
    	
    	session.deleteTemporaryMaterial();
    	
    	
		/* Return an indication that the protocol is completed, possibly with the provided External Authorization Data */
		
		// A CBOR byte string with zero length, indicating that the protocol has successfully completed
		byte[] reply = new byte[] {};
		processingResult.add(CBORObject.FromObject(reply));

		// The Connection Identifier C_R used by the Responder
		CBORObject connectionIdentifierResponderCbor = CBORObject.FromObject(connectionIdentifierResponder);
		processingResult.add(connectionIdentifierResponderCbor);
		
		// External Authorization Data from EAD_3 (if present)
		if (ead3 != null) {
		    CBORObject eadArray = CBORObject.NewArray();
		    for (int i = 0; i < ead3.length; i++) {
		        eadArray.Add(ead3[i]);
		    }
		    processingResult.add(eadArray);
		}
		
		session.setCurrentStep(Constants.EDHOC_AFTER_M3);
		
		System.out.println("\nCompleted processing of EDHOC Message 3\n");
		return processingResult;
		
	}
	
	
    /**
     *  Process an EDHOC Message 4
     * @param sequence   The CBOR sequence used as payload of the EDHOC Message 4
     * @param isReq   True if the CoAP message is a request, or False otherwise
     * @param connectionIdInitiator   The already known connection identifier of the Initiator.
     * 								  It is set to null if expected in the EDHOC Message 4.
     * @param edhocSessions   The list of active EDHOC sessions of the recipient
     * @param usedConnectionIds   The set of already allocated Connection Identifiers
     * @return   A list of CBOR Objects including up to two elements.
     * 
     *           The first element is a CBOR byte string, with value either:
     *              i) a zero length byte string, indicating that the EDHOC message_4 was successfully processed; or
     *             ii) a non-zero length byte string as the EDHOC Error Message to be sent.
     *
     *           In case (ii), the second element is a CBOR integer, with value the CoAP response code
     *           to use for the EDHOC Error Message, if this is a CoAP response.
     */
	public static List<CBORObject> readMessage4(byte[] sequence, boolean isReq, byte[] connectionIdInitiator,
												HashMap<CBORObject,EdhocSession> edhocSessions,
			                                    Set<CBORObject> usedConnectionIds) {
		
		if (sequence == null || edhocSessions == null || usedConnectionIds == null)
			return null;

		byte[] connectionIdentifierInitiator = null; // The connection identifier of the Initiator
		byte[] connectionIdentifierResponder = null; // The connection identifier of the Responder
		
		CBORObject[] ead4 = null; // Will be set if External Authorization Data is present as EAD4
		
		List<CBORObject> processingResult = new ArrayList<CBORObject>(); // List of CBOR Objects to return as result
		
		boolean error = false; // Will be set to True if an EDHOC Error Message has to be returned
		int errorCode = Constants.ERR_CODE_UNSPECIFIED_ERROR; // The error code to use for the EDHOC Error Message
		ResponseCode responseCode = null; // Will be set to the CoAP response code to use for the EDHOC Error Message
		String errMsg = null; // The text string to be possibly returned as DIAG_MSG in an EDHOC Error Message
		EdhocSession session = null; // The session used for this EDHOC execution
		
		int index = 0;
		CBORObject[] objectList = null;
		try {
			objectList = CBORObject.DecodeSequenceFromBytes(sequence);
		}
		catch (Exception e) {
		    errMsg = new String("Malformed or invalid EDHOC message_4");
		    responseCode = ResponseCode.BAD_REQUEST;
		    error = true;
		}

		
		/* Consistency checks */
		
		// If EDHOC Message 4 is transported in a CoAP request, C_I is present as first element of the CBOR sequence
		if (error == false && isReq == true) {
			if (error == false && objectList[index].getType() != CBORType.ByteString &&
				objectList[index].getType() != CBORType.Integer)  {
					errMsg = new String("C_I must be a byte string or an integer");
					responseCode = ResponseCode.BAD_REQUEST;
					error = true;
			}

			if (error == false) {
				connectionIdentifierInitiator = decodeIdentifier(objectList[index]);
				if (connectionIdentifierInitiator == null) {
				    errMsg = new String("Invalid encoding of C_I");
				    responseCode = ResponseCode.BAD_REQUEST;
				    error = true;
				}
				else {
					index++;
					if (debugPrint) {
					    Util.nicePrint("Connection Identifier of the Initiator", connectionIdentifierInitiator);
					}
				}
			}
			
		}
		
		if (error == false && isReq == false) {
			connectionIdentifierInitiator = connectionIdInitiator;
			if (debugPrint) {
			    Util.nicePrint("Connection Identifier of the Initiator", connectionIdentifierInitiator);
			}
		}
		
		if (error == false) {
			CBORObject connectionIdentifierInitiatorCbor = CBORObject.FromObject(connectionIdentifierInitiator);
			session = edhocSessions.get(connectionIdentifierInitiatorCbor);
			
			if (session == null) {
				errMsg = new String("EDHOC session not found");
				responseCode = ResponseCode.BAD_REQUEST;
				error = true;
			}
			else if (session.isInitiator() == false) {
				errMsg = new String("EDHOC Message 4 is intended only to an Initiator");
				responseCode = ResponseCode.BAD_REQUEST;
				error = true;
			}
			else if (session.getApplicationProfile().getUseMessage4() == false) {
				errMsg = new String("EDHOC Message 4 is not used for this application profile");
				responseCode = ResponseCode.BAD_REQUEST;
				error = true;
			}
			else if (session.getCurrentStep() != Constants.EDHOC_SENT_M3) {
				errMsg = new String("The protocol state is not waiting for an EDHOC Message 4");
				responseCode = ResponseCode.BAD_REQUEST;
				error = true;
			}
		}
		
		if (session != null && session.getPeerConnectionId() != null)
				connectionIdentifierResponder = session.getPeerConnectionId();


		// CIPHERTEXT_4
		byte[] ciphertext4 = null;
		if (error == false && objectList[index].getType() != CBORType.ByteString) {
			errMsg = new String("CIPHERTEXT_4 must be a byte string");
			responseCode = ResponseCode.BAD_REQUEST;
			error = true;
		}
		else {
			ciphertext4 = objectList[index].GetByteString();
			if (ciphertext4 == null) {
				errMsg = new String("Error when retrieving CIPHERTEXT_4");
				responseCode = ResponseCode.BAD_REQUEST;
				error = true;
			}
		}
		
		/* Return an EDHOC Error Message */
		
		if (error == true) {
			Util.purgeSession(session, connectionIdentifierInitiator, edhocSessions, usedConnectionIds);
			return processError(errorCode, Constants.EDHOC_MESSAGE_4, !isReq, connectionIdentifierResponder,
								errMsg, null, responseCode);
		}

		
		
		/* Compute the plaintext */
		
		if (debugPrint && ciphertext4 != null) {
		    Util.nicePrint("CIPHERTEXT_4", ciphertext4);
		}
		
		// Prepare the External Data as including only TH4
    	byte[] externalData = session.getTH4();

    	if (externalData == null) {
    		errMsg = new String("Error when computing the external data for CIPHERTEXT_4");
    		responseCode = ResponseCode.INTERNAL_SERVER_ERROR;
    		error = true;
    	}
    	else if (debugPrint) {
    		Util.nicePrint("External Data to compute CIPHERTEXT_4", externalData);
    	}
    	    	
    	
        // Compute the key material
      
    	// Compute K and IV to protect the COSE object
    	
    	byte[] k4ae = computeKey(Constants.EDHOC_K_4, session);
    	if (k4ae == null) {
    		errMsg = new String("Error when computing K");
    		responseCode = ResponseCode.INTERNAL_SERVER_ERROR;
    		error = true;
    	}
    	else if (debugPrint) {
    		Util.nicePrint("K", k4ae);
    	}

    	byte[] iv4ae = computeIV(Constants.EDHOC_IV_4, session);
    	if (iv4ae == null) {
    		errMsg = new String("Error when computing IV");
    		responseCode = ResponseCode.INTERNAL_SERVER_ERROR;
    		error = true;
    	}
    	else if (debugPrint) {
    		Util.nicePrint("IV", iv4ae);
    	}
        
    	byte[] plaintext4 = decryptCiphertext4(session, externalData, ciphertext4, k4ae, iv4ae);
    	if (plaintext4 == null) {
    	    errMsg = new String("Error when decrypting CIPHERTEXT_4");
    	    responseCode = ResponseCode.INTERNAL_SERVER_ERROR;
    	    Util.purgeSession(session, connectionIdentifierInitiator, edhocSessions, usedConnectionIds);
    	    return processError(errorCode, Constants.EDHOC_MESSAGE_4, !isReq, connectionIdentifierResponder,
    	    					errMsg, null, responseCode);
    	}
    	else if (debugPrint) {
    	    Util.nicePrint("Plaintext retrieved from CIPHERTEXT_4", plaintext4);
    	}
    	
        /* End computing the plaintext */
    	
    	
    	// Parse the outer plaintext as a CBOR sequence. To be valid, this is
    	// either the empty plaintext or the External Authorization Data EAD_4 
    	error = false;
    	CBORObject[] plaintextElementList = null;
    	
    	if (plaintext4.length != 0) {
    		try {
    			plaintextElementList = CBORObject.DecodeSequenceFromBytes(plaintext4);
    		}
    		catch (Exception e) {
        		errMsg = new String("Malformed or invalid EAD_4");
        		responseCode = ResponseCode.BAD_REQUEST;
    			error = true;
    		}
    		
    		if (error == false && plaintextElementList.length > 0) {
			   // EAD_4 is present
			   ead4 = preParseEAD(plaintextElementList, 0, 4, session.getSupportedEADs());
			   
			   if (ead4.length == 1 && ead4[0].getType() == CBORType.TextString) {
			         errMsg = new String(ead4[0].toString());
			         responseCode = ResponseCode.BAD_REQUEST;
				     ead4 = null;
			         error = true;
			   }
    		}
    	}
    	    	

		SideProcessor sideProcessor = session.getSideProcessor();
		
		// Invoke the processing of EAD items in EAD_4 (if any)
		if (error == false && ead4!= null && ead4.length != 0) {

			sideProcessor.sideProcessingMessage4(ead4);
			
			// A fatal error occurred
			if (sideProcessor.getResults(Constants.EDHOC_MESSAGE_4, false).
					containsKey(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_ERROR))) {
				errMsg = new String(sideProcessor.getResults(Constants.EDHOC_MESSAGE_4, false).
						get(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_ERROR)).get(0).
						get(Integer.valueOf(Constants.SIDE_PROCESSOR_INNER_ERROR_DESCRIPTION)).AsString());
				int responseCodeValue = sideProcessor.getResults(Constants.EDHOC_MESSAGE_4, false).
			            get(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_ERROR)).get(0).
			            get(Integer.valueOf(Constants.SIDE_PROCESSOR_INNER_ERROR_RESP_CODE)).AsInt32();
				responseCode = ResponseCode.valueOf(responseCodeValue);
				error = true;
				
				// No need to keep this information any longer in the side processor object
				sideProcessor.removeResultSet(Constants.EDHOC_MESSAGE_4, Constants.SIDE_PROCESSOR_OUTER_ERROR, false);
			}
		}
		
		// If EAD_4 was present, print any available results from processing its EAD items.
		sideProcessor.showResultsFromSideProcessing(Constants.EDHOC_MESSAGE_4, false);
    	    	
    	
    	// Return an EDHOC Error Message
    	if (error == true) {
        	Util.purgeSession(session, connectionIdentifierInitiator, edhocSessions, usedConnectionIds);
			return processError(errorCode, Constants.EDHOC_MESSAGE_4, !isReq, connectionIdentifierResponder,
								errMsg, null, responseCode);
    	}

    	session.setPRK4e3m(null);
    	session.setTH4(null);
    	
		
		/* Return an indication that message_4 is fine */
		
		// A CBOR byte string with zero length, indicating that the EDHOC Message 4 is fine
		byte[] reply = new byte[] {};
		processingResult.add(CBORObject.FromObject(reply));
		
		// External Authorization Data from EAD_4 (if present)
		if (ead4 != null) {
		    CBORObject eadArray = CBORObject.NewArray();
		    for (int i = 0; i< ead4.length; i++) {
		        eadArray.Add(ead4[i]);
		    }
		    processingResult.add(eadArray);
		}
				
    	session.setCurrentStep(Constants.EDHOC_AFTER_M4);
		
		System.out.println("\nCompleted processing of EDHOC Message 4");
		return processingResult;
		
	}
	
	
    /**
     *  Parse an EDHOC Error Message
     * @param sequence   The CBOR sequence used as payload of the EDHOC Error Message
     * @param cX   The connection identifier of the recipient; set to null if expected in the EDHOC Error Message
     * @param edhocSessions   The list of active EDHOC sessions of the recipient
     * @return  The elements of the EDHOC Error Message as CBOR objects, or null in case of errors
     */
	public static CBORObject[] readErrorMessage(byte[] sequence, byte[] connectionIdentifier,
												HashMap<CBORObject, EdhocSession> edhocSessions) {
		
		if (edhocSessions == null || sequence == null) {
			System.err.println("Error when processing EDHOC Error Message");
			return null;
		}
		
		int index = 0;
		EdhocSession mySession = null;
		CBORObject[] objectList = null;
		try {
			objectList = CBORObject.DecodeSequenceFromBytes(sequence);
		}
		catch (Exception e) {
			System.err.println("Malformed or invalid EDHOC Error Message");
			return null;
		}
		
		if (objectList.length == 0 || objectList.length > 3) {
			System.err.println("Error when processing EDHOC Error Message - Zero or too many elements");
			return null;
		}

		// The connection identifier of the recipient is provided by the method caller
		if (connectionIdentifier != null) {
			CBORObject connectionIdentifierCbor = CBORObject.FromObject(connectionIdentifier);
			mySession = edhocSessions.get(connectionIdentifierCbor);
		}
		// The connection identifier is expected as first element in the EDHOC Error Message
		else {
			if (objectList[index].getType() == CBORType.ByteString || objectList[index].getType() == CBORType.Integer) {
				byte[] retrievedConnectionIdentifier = decodeIdentifier(objectList[index]);
				if (retrievedConnectionIdentifier != null) {
					CBORObject connectionIdentifierCbor = CBORObject.FromObject(retrievedConnectionIdentifier);
					mySession = edhocSessions.get(connectionIdentifierCbor);
					index++;		
				}
			}
			else {
				System.err.println("Error when processing EDHOC Error Message - Invalid format of C_X");
				return null;
			}
			
		}
		
		// No session for this Connection Identifier
		if (mySession == null) {
			System.err.println("Error when processing EDHOC Error Message - Impossible to retrieve a session from C_X");
			return null;
		}
		
		boolean initiator = mySession.isInitiator();
		
		if (objectList[index].getType() != CBORType.Integer) {
			System.err.println("Error when processing EDHOC Error Message - Invalid format of ERR_CODE");
			return null;
		}
		
		// Retrieve ERR_CODE
		int errorCode = objectList[index].AsInt32();
		index++;
		
		// Check that the rest of the message is consistent
		if (objectList.length == index){
			System.err.println("Error when processing EDHOC Error Message - ERR_INFO expected but not included");
			return null;
		}
		if (objectList.length > (index + 1)){
			System.err.println("Error when processing EDHOC Error Message - Unexpected parameters following ERR_INFO");
			return null;
		}
		if (errorCode == Constants.ERR_CODE_SUCCESS) {
			// This is not admitted
			System.err.println("Received EDHOC error message with ERR_CODE 0");
			return null;
		}
		else if (errorCode == Constants.ERR_CODE_UNSPECIFIED_ERROR) {
			if (objectList[index].getType() != CBORType.TextString) {
				System.err.println("Error when processing EDHOC Error Message - Invalid format of ERR_INFO");
				return null;
			}
		}
		else if (errorCode == Constants.ERR_CODE_WRONG_SELECTED_CIPHER_SUITE) {
			if (initiator == true && mySession.getCurrentStep() == Constants.EDHOC_SENT_M1) {
				if (objectList[index].getType() != CBORType.Array && objectList[index].getType() != CBORType.Integer) {
					System.err.println("Error when processing EDHOC Error Message - Invalid format for SUITES_R");
					return null;
				}
				if (objectList[index].getType() == CBORType.Array) {
					for (int i = 0; i < objectList[index].size(); i++) {
						if (objectList[index].get(i).getType() != CBORType.Integer) {
							System.err.println("Error when processing EDHOC Error Message - " +
											   "Invalid format for elements of SUITES_R");
							return null;
						}
					}
				}
			}
			else {
				System.err.println("Unexpected EDHOC Error Message with Error Code " +
								    Constants.ERR_CODE_WRONG_SELECTED_CIPHER_SUITE +
								    " (Wrong selected cipher suite)");
				return null;
			}
			
		}
		else {
			// Unknown error code
			System.err.println("Unknown error code in EDHOC Error Message: " + errorCode);
			return null;
		}
		
		return objectList;
		
	}
	
    /**
     *  Write an EDHOC Message 1
     * @param session   The EDHOC session associated to this EDHOC message
     * @return  The raw payload to transmit as EDHOC Message 1, or null in case of errors
     */
	public static byte[] writeMessage1(EdhocSession session) {
		
        // Prepare the list of CBOR objects to build the CBOR sequence
        List<CBORObject> objectList = new ArrayList<>();
        
        // C_X equal to the CBOR simple value 'true' (i.e., 0xf5), if EDHOC message_1 is transported in a CoAP request
        if (session.isClientInitiated() == true) {
        	objectList.add(CBORObject.True);
        }
        
        // METHOD as CBOR integer
        int method = session.getMethod();
        objectList.add(CBORObject.FromObject(method));
        if (debugPrint) {
        	System.out.println("===================================");
        	System.out.println("EDHOC Message 1 content:\n");
        	CBORObject obj = CBORObject.FromObject(method);
        	byte[] objBytes = obj.EncodeToBytes();
        	Util.nicePrint("METHOD", objBytes);
        }
        
        // SUITES_I as CBOR integer or CBOR array
        List<Integer> supportedCipherSuites = session.getSupportedCipherSuites();
        List<Integer> peerSupportedCipherSuites = session.getPeerSupportedCipherSuites();
        
    	int selectedSuite = -1;
    	int preferredSuite = supportedCipherSuites.get(0).intValue();
    	
    	// No SUITES_R has been received, so it is not known what cipher suites the responder supports
    	if (peerSupportedCipherSuites.size() == 0) {
    		// The selected cipher suite is the most preferred by the initiator
    		selectedSuite = preferredSuite;
    	}
    	// SUITES_R has been received, so it is known what cipher suites the responder supports
    	else {
    		// Pick the selected cipher suite as the most preferred by the Initiator from the ones supported by the Responder
    		for (Integer i : supportedCipherSuites) {
    			if (peerSupportedCipherSuites.contains(i)) {
    				selectedSuite = i.intValue();
    				break;
    			}
    		}
    	}
    	if (selectedSuite == -1) {
    		System.err.println("Impossible to agree on a mutually supported cipher suite");
    		return null;
    	}
    	
		// Set the selected cipher suite
    	session.setSelectedCipherSuite(selectedSuite);
    	
		// Set the asymmetric key pair, CRED and ID_CRED of the Initiator to use in this session
    	session.setAuthenticationCredential();
    	
    	// Set the ephemeral keys of the Initiator to use in this session
    	if (session.getEphemeralKey() == null)
    		session.setEphemeralKey();
    	
    	CBORObject suitesI;
    	if (selectedSuite == preferredSuite) {
    		// SUITES_I is only the selected suite, as a CBOR integer
    		suitesI = CBORObject.FromObject(selectedSuite);
    	}
    	else {
    		// SUITES_I is a CBOR array
    		// The elements are the Initiator's supported cipher suite in decreasing order of preference,
    		// up until and including the selected suite as last element of the array.
    		suitesI = CBORObject.NewArray();
    		for (Integer i : supportedCipherSuites) {
    			int suite = i.intValue();
    			suitesI.Add(suite);
    			if (suite == selectedSuite) {
    				break;
    			}
    		}
    	}
    	objectList.add(suitesI);
        if (debugPrint) {
        	byte[] objBytes = suitesI.EncodeToBytes();
        	Util.nicePrint("SUITES_I", objBytes);
        }

        // G_X as a CBOR byte string
        CBORObject gX = null;
		if (selectedSuite == Constants.EDHOC_CIPHER_SUITE_0 || selectedSuite == Constants.EDHOC_CIPHER_SUITE_1) {
			gX = session.getEphemeralKey().PublicKey().get(KeyKeys.OKP_X);
		}
		else if (selectedSuite == Constants.EDHOC_CIPHER_SUITE_2 || selectedSuite == Constants.EDHOC_CIPHER_SUITE_3) {
			gX = session.getEphemeralKey().PublicKey().get(KeyKeys.EC2_X);
		}
		objectList.add(gX);
        if (debugPrint) {
        	Util.nicePrint("G_X", gX.GetByteString());
        }
		
		// C_I
        byte[] connectionIdentifierInitiator = session.getConnectionId();
        CBORObject cI = encodeIdentifier(connectionIdentifierInitiator);
		objectList.add(cI);
        if (debugPrint) {
           	Util.nicePrint("Connection Identifier of the Initiator", connectionIdentifierInitiator);
        	Util.nicePrint("C_I", cI.EncodeToBytes());
        }

        SideProcessor sideProcessor = session.getSideProcessor();
        
        // Produce possible EAD items following early instructions from the application
        sideProcessor.produceIndependentEADs(Constants.EDHOC_MESSAGE_1);
        
        // A fatal error occurred
        if (sideProcessor.getResults(Constants.EDHOC_MESSAGE_1, false).
                containsKey(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_ERROR))) {
            
        	String errMsg = new String(sideProcessor.getResults(Constants.EDHOC_MESSAGE_1, false).
                    get(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_ERROR)).get(0).
                    get(Integer.valueOf(Constants.SIDE_PROCESSOR_INNER_ERROR_DESCRIPTION)).AsString());
        	
    		System.err.println(errMsg);
    		
            // No need to keep this information any longer in the side processor object
            sideProcessor.removeResultSet(Constants.EDHOC_MESSAGE_1, Constants.SIDE_PROCESSOR_OUTER_ERROR, false);
            
    		return null;
        }
        
        List<CBORObject> ead1 = session.getSideProcessor().getProducedEADs(Constants.EDHOC_MESSAGE_1);
        
        // There are EAD items to include in EAD_1
        if (ead1 != null && ead1.size() != 0) {
        	for (int i = 0; i < ead1.size(); i++) {
        		objectList.add(ead1.get(i));
        	}
        }
        if (debugPrint) {
        	System.out.println("===================================");
        }
		
        
    	/* Prepare EDHOC Message 1 */
    	
    	if (debugPrint) {
    	    byte[] sequenceBytesToPrint;
    	    byte[] sequenceBytes = Util.buildCBORSequence(objectList);
    	    
    	    // If EDHOC message_1 is transported in a CoAP request, do not print
    	    // the prepended C_X equal to the CBOR simple value 'true' (i.e., 0xf5)
    	    if (session.isClientInitiated() == true) {
    	        List<CBORObject> trimmedSequence = new ArrayList<CBORObject>();
    	        for (int i = 1; i < objectList.size(); i++) {
    	            trimmedSequence.add(objectList.get(i));
    	        }
    	        sequenceBytesToPrint = Util.buildCBORSequence(trimmedSequence);
    	    }
    	    else {
    	        sequenceBytesToPrint = sequenceBytes;
    	    }
    	    Util.nicePrint("EDHOC Message 1", sequenceBytesToPrint);
    	}
        
        return Util.buildCBORSequence(objectList);
		
	}

	
    /**
     *  Write an EDHOC Message 2
     * @param session   The EDHOC session associated to this EDHOC message
     * @return  The raw payload to transmit as EDHOC Message 2 or EDHOC Error Message; or null in case of errors
     */
	public static byte[] writeMessage2(EdhocSession session) {
		
		List<CBORObject> objectList = new ArrayList<>();
		boolean error = false; // Will be set to True if an EDHOC Error Message has to be returned
		int errorCode = Constants.ERR_CODE_UNSPECIFIED_ERROR; // The error code to use for the EDHOC Error Message
		ResponseCode responseCode = ResponseCode.INTERNAL_SERVER_ERROR;; // The CoAP response code to use for the EDHOC Error Message
		String errMsg = null; // The text string to be possibly returned as DIAG_MSG in an EDHOC Error Message
		
        if (debugPrint) {
        	System.out.println("===================================");
        	System.out.println("Start processing EDHOC Message 2:\n");
        }
		
        
        // C_I, if EDHOC message_2 is transported in a CoAP request
		if (!session.isClientInitiated()) {
			byte[] connectionIdentifierInitiator = session.getPeerConnectionId(); 
			CBORObject cI = encodeIdentifier(connectionIdentifierInitiator);
			objectList.add(cI);
	        if (debugPrint) {
	        	Util.nicePrint("Connection Identifier of the Initiator", connectionIdentifierInitiator);
	        	Util.nicePrint("C_I", cI.EncodeToBytes());
	        }
		}
		
    	// Set the ephemeral keys of the Responder to use in this session
    	if (session.getEphemeralKey() == null)
    		session.setEphemeralKey();
		
		// G_Y as a CBOR byte string
		int selectedSuite = session.getSelectedCipherSuite();
        CBORObject gY = null;
        if (selectedSuite == Constants.EDHOC_CIPHER_SUITE_0 || selectedSuite == Constants.EDHOC_CIPHER_SUITE_1) {
			gY = session.getEphemeralKey().PublicKey().get(KeyKeys.OKP_X);
		}
		else if (selectedSuite == Constants.EDHOC_CIPHER_SUITE_2 || selectedSuite == Constants.EDHOC_CIPHER_SUITE_3) {
			gY = session.getEphemeralKey().PublicKey().get(KeyKeys.EC2_X);
		}
    	if (debugPrint) {
    	    Util.nicePrint("G_Y", gY.GetByteString());
    	}
		
		// C_R
    	byte[] connectionIdentifierResponder = session.getConnectionId(); 
		CBORObject cR = encodeIdentifier(connectionIdentifierResponder);
    	if (debugPrint) {
    		Util.nicePrint("Connection Identifier of the Responder", connectionIdentifierResponder);
    	    Util.nicePrint("C_R", cR.EncodeToBytes());
    	}
    	
    	
        // Compute TH_2
        
        byte[] hashMessage1 = session.getHashMessage1(); // the hash of message_1, as plain bytes
        byte[] hashMessage1SerializedCBOR = CBORObject.FromObject(hashMessage1).EncodeToBytes();
        byte[] gYSerializedCBOR = gY.EncodeToBytes();
        
        byte[] th2 = computeTH2(session, gYSerializedCBOR, hashMessage1SerializedCBOR);
        if (th2 == null) {
    		System.err.println("Error when computing TH_2");
    		errMsg = new String("Error when computing TH_2");
    		error = true;
        }
        else if (debugPrint) {
        	Util.nicePrint("H(message_1)", hashMessage1);
    		Util.nicePrint("TH_2", th2);
    	}
        session.setTH2(th2);
        session.cleanMessage1();
        

        // Compute the key material
        
        byte[] prk2e = null;
        byte[] prk3e2m = null;
        
        // Compute the Diffie-Hellman secret G_XY
        byte[] dhSecret = SharedSecretCalculation.generateSharedSecret(session.getEphemeralKey(),
        		                                                       session.getPeerEphemeralPublicKey());
    	
        if (dhSecret == null) {
    		System.err.println("Error when computing the Diffie-Hellman Secret");
    		errMsg = new String("Error when computing the Diffie-Hellman Secret");
    		error = true;
        }
        else if (debugPrint) {
    		Util.nicePrint("G_XY", dhSecret);
    	}
        
        // Compute PRK_2e
        if (error == false) {
	        String hashAlgorithm = EdhocSession.getEdhocHashAlg(session.getSelectedCipherSuite());
	    	
	    	prk2e = computePRK2e(th2, dhSecret, hashAlgorithm);
	    	
	        dhSecret = null;
	    	if (prk2e == null) {
	    		System.err.println("Error when computing PRK_2e");
	    		errMsg = new String("Error when computing PRK_2e");
	    		error = true;
	    	}
	    	else if (debugPrint) {
	    		Util.nicePrint("PRK_2e", prk2e);
	    	}
	        session.setPRK2e(prk2e);
        }
        
        // Compute PRK_3e2m
        if (error == false) {
	    	prk3e2m = computePRK3e2m(session, prk2e);
	    	if (prk3e2m == null) {
	    		System.err.println("Error when computing PRK_3e2m");
	    		errMsg = new String("Error when computing PRK_3e2m");
	    		error = true;
	    	}
	    	else if (debugPrint) {
	    		Util.nicePrint("PRK_3e2m", prk3e2m);
	    	}
	    	session.setPRK3e2m(prk3e2m);
        }
    	
        // Produce possible EAD items following early instructions from the application
        if (error == false) {
            SideProcessor sideProcessor = session.getSideProcessor();
            
            sideProcessor.produceIndependentEADs(Constants.EDHOC_MESSAGE_2);
            
    	    // A fatal error occurred
    	    if (sideProcessor.getResults(Constants.EDHOC_MESSAGE_2, false).
    	    		containsKey(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_ERROR))) {
    	    	errMsg = new String(sideProcessor.getResults(Constants.EDHOC_MESSAGE_2, false).
    	        	get(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_ERROR)).get(0).
    	            get(Integer.valueOf(Constants.SIDE_PROCESSOR_INNER_ERROR_DESCRIPTION)).AsString());
    	        int responseCodeValue = sideProcessor.getResults(Constants.EDHOC_MESSAGE_2, false).
    	        	get(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_ERROR)).get(0).
    	            get(Integer.valueOf(Constants.SIDE_PROCESSOR_INNER_ERROR_RESP_CODE)).AsInt32();
    	        responseCode = ResponseCode.valueOf(responseCodeValue);
    	        error = true;
    	         
    	        // No need to keep this information any longer in the side processor object
    	        sideProcessor.removeResultSet(Constants.EDHOC_MESSAGE_2, Constants.SIDE_PROCESSOR_OUTER_ERROR, false);
    	    }
        }
        CBORObject ead2[] = null;
        if (error == false) {
    	    List<CBORObject> myList = session.getSideProcessor().getProducedEADs(Constants.EDHOC_MESSAGE_2);

    		// There are EAD items to include in EAD_2
    		if (myList != null && myList.size() != 0) {
    			ead2 = new CBORObject[myList.size()];
    		    for (int i = 0; i < myList.size(); i++) {
    		   	 ead2[i] = myList.get(i);
    		    }
    		}
        }
        
        
    	/* Start computing Signature_or_MAC_2 */    	
    	
        byte[] mac2 = null;

    	// Compute MAC_2
        if (error == false) {       	
	    	mac2 = computeMAC2(session, prk3e2m, th2, session.getIdCred(), session.getCred(), ead2);
	    	if (mac2 == null) {
	    		System.err.println("Error when computing MAC_2");
	    		errMsg = new String("Error when computing MAC_2");
	    		error = true;
	    	}
	    	else if (debugPrint) {
	    		Util.nicePrint("MAC_2", mac2);
	    	}
        }
    	
    	// Compute Signature_or_MAC_2
    	
        // Compute the external data for the external_aad, as a CBOR sequence
        byte[] externalData = null;
        if (error == false) {
			externalData = computeExternalData(th2, session.getCred(), ead2);
			if (externalData == null) {
				System.err.println("Error when computing the external data for MAC_2");
				errMsg = new String("Error when computing the external data for MAC_2");
				error = true;
			}
        }
    	
    	byte[] signatureOrMac2 = computeSignatureOrMac2(session, mac2, externalData);
        if (error == false) {
			if (signatureOrMac2 == null) {
				System.err.println("Error when computing Signature_or_MAC_2");
				errMsg = new String("Error when computing Signature_or_MAC_2");
				error = true;
			}
			else if (debugPrint) {
				Util.nicePrint("Signature_or_MAC_2", signatureOrMac2);
			}
        }
    	
    	/* End computing Signature_or_MAC_2 */ 
    
    	
    	/* Start computing CIPHERTEXT_2 */
    
        byte[] plaintext2 = null;
    	byte[] keystream2 = null;
        if (error == false) {
	    	// Prepare the plaintext
	    	List<CBORObject> plaintextElementList = new ArrayList<>();
	    	plaintextElementList.add(cR);
	    	CBORObject plaintextElement = null;
	    	if (session.getIdCred().ContainsKey(HeaderKeys.KID.AsCBOR())) {
	    		// ID_CRED_R uses 'kid', whose value is the only thing to include in the plaintext
	    		CBORObject kid = session.getIdCred().get(HeaderKeys.KID.AsCBOR());
	    		plaintextElement = MessageProcessor.encodeIdentifier(kid.GetByteString());
	    	}
	    	else {
	    		plaintextElement = session.getIdCred();
	    	}
	    	plaintextElementList.add(plaintextElement);
	    	plaintextElementList.add(CBORObject.FromObject(signatureOrMac2));
	    	if (ead2 != null) {
	    		for (int i = 0; i < ead2.length; i++)
	    			plaintextElementList.add(ead2[i]);
	    	}
	    	plaintext2 = Util.buildCBORSequence(plaintextElementList);
	    	if (debugPrint && plaintext2 != null) {
	    		Util.nicePrint("Plaintext to compute CIPHERTEXT_2", plaintext2);
	    	}
	    	
	    	// Compute KEYSTREAM_2
	        if (error == false) {
		    	keystream2 = computeKeystream2(session, plaintext2.length);
		    	if (keystream2== null) {
		    		System.err.println("Error when computing KEYSTREAM_2");
		    		errMsg = new String("Error when computing KEYSTREAM_2");
		    		error = true;
		    	}
		    	else if (debugPrint) {
		    		Util.nicePrint("KEYSTREAM_2", keystream2);
		    	}
	        }
		}
        
    	
    	// Compute CIPHERTEXT_2
	    if (error == false) {
	        
	    	byte[] ciphertext2 = Util.arrayXor(plaintext2, keystream2);

	    	// Store PLAINTEXT_2 for the later computation of TH_3
	    	session.setPlaintext2(plaintext2);
	    	
	    	if (debugPrint && ciphertext2 != null) {
	    		Util.nicePrint("CIPHERTEXT_2", ciphertext2);
	    	}
	
	    	/* End computing CIPHERTEXT_2 */
	    	
	    	
	    	// Finish building the outer CBOR sequence
	    	
	    	// Concatenate G_Y with CIPHERTEXT_2
	    	byte[] gY_Ciphertext2 = new byte[gY.GetByteString().length + ciphertext2.length];
	    	System.arraycopy(gY.GetByteString(), 0, gY_Ciphertext2, 0, gY.GetByteString().length);
	    	System.arraycopy(ciphertext2, 0, gY_Ciphertext2, gY.GetByteString().length, ciphertext2.length);
	    	
	    	// Wrap the result in a single CBOR byte string, included in the outer CBOR sequence of EDHOC Message 2
	    	objectList.add(CBORObject.FromObject(gY_Ciphertext2));
	    	if (debugPrint) {
	    	    Util.nicePrint("G_Y | CIPHERTEXT_2", gY_Ciphertext2);
	    	}

        }

    	
		/* Prepare an EDHOC Error Message */
		
		if (error == true) {
			byte[] connectionIdentifierInitiator = session.getPeerConnectionId();
			
			List<CBORObject> processingResult = processError(errorCode, Constants.EDHOC_MESSAGE_1,
															 !session.isClientInitiated(),
															 connectionIdentifierInitiator,
															 errMsg, null, responseCode);
			return processingResult.get(0).GetByteString();
		}
    	
    	
    	/* Prepare EDHOC Message 2 */
    	
		if (debugPrint) {
		    byte[] sequenceBytesToPrint;
		    byte[] sequenceBytes = Util.buildCBORSequence(objectList);
		    
		    // If EDHOC message_2 is transported in a CoAP request, do not print the prepended C_I
		    if (session.isClientInitiated() == false) {
		    	List<CBORObject> trimmedSequence = new ArrayList<CBORObject>();
		    	for (int i = 1; i < objectList.size(); i++) {
		    		trimmedSequence.add(objectList.get(i));
		    	}
		        sequenceBytesToPrint = Util.buildCBORSequence(trimmedSequence);
		    }
		    else {
		        sequenceBytesToPrint = sequenceBytes;
		    }
		    Util.nicePrint("EDHOC Message 2", sequenceBytesToPrint);
		}
		
        return Util.buildCBORSequence(objectList);
		
	}
	
    /**
     *  Write an EDHOC Message 3
     * @param session   The EDHOC session associated to this EDHOC message
     * @return  The raw payload to transmit as EDHOC Message 3 or EDHOC Error Message; or null in case of errors
     */
	public static byte[] writeMessage3(EdhocSession session) {
		
		List<CBORObject> objectList = new ArrayList<>();
		boolean error = false; // Will be set to True if an EDHOC Error Message has to be returned
		int errorCode = Constants.ERR_CODE_UNSPECIFIED_ERROR; // The error code to use for the EDHOC Error Message
		ResponseCode responseCode = ResponseCode.INTERNAL_SERVER_ERROR; // The CoAP response code to use for the EDHOC Error Message
		String errMsg = null; // The text string to be possibly returned as DIAG_MSG in an EDHOC Error Message
		
        if (debugPrint) {
        	System.out.println("===================================");
        	System.out.println("Start processing EDHOC Message 3:\n");
        }
		
        /* Start preparing data_3 */
		
        // C_R, if EDHOC message_3 is transported in a CoAP request
		if (session.isClientInitiated()) {
			byte[] connectionIdentifierResponder = session.getPeerConnectionId();
			CBORObject cR = encodeIdentifier(connectionIdentifierResponder);
			objectList.add(cR);
	        if (debugPrint) {
	        	Util.nicePrint("Connection Identifier of the Responder", connectionIdentifierResponder);
	        	Util.nicePrint("C_R", cR.EncodeToBytes());
	        }
		}
        
		
		/* End preparing data_3 */
		
		
		/* Start computing the inner COSE object */
		
        // Compute TH_3
        
        byte[] th2 = session.getTH2(); // TH_2 as plain bytes
        byte[] th2SerializedCBOR = CBORObject.FromObject(th2).EncodeToBytes();

        byte[] plaintext2 = session.getPlaintext2(); // PLAINTEXT_2 as serialized CBOR Sequence
        byte[] credR = session.getPeerCred();
        
        byte[] th3 = computeTH3(session, th2SerializedCBOR, plaintext2, credR);
        
        if (th3 == null) {
        	System.err.println("Error when computing TH_3");
    		errMsg = new String("Error when computing TH_3");
    		error = true;
        }
        else if (debugPrint) {
    		Util.nicePrint("TH_3", th3);
    	}
        session.setTH3(th3);

        // Compute the key material
        byte[] prk4e3m = null;
        if (error == false) {
	        prk4e3m = computePRK4e3m(session);
	    	if (prk4e3m == null) {
	    		System.err.println("Error when computing PRK_4e3m");
	    		errMsg = new String("Error when computing PRK_4e3m");
	    		error = true;
	    	}
	    	else if (debugPrint) {
	    		Util.nicePrint("PRK_4e3m", prk4e3m);
	    	}
	    	session.setPRK4e3m(prk4e3m);
        }
        
        // Produce possible EAD items following early instructions from the application
        if (error == false) {
            SideProcessor sideProcessor = session.getSideProcessor();
            
            sideProcessor.produceIndependentEADs(Constants.EDHOC_MESSAGE_3);
            
            // A fatal error occurred
            if (sideProcessor.getResults(Constants.EDHOC_MESSAGE_3, false).
                    containsKey(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_ERROR))) {
                errMsg = new String(sideProcessor.getResults(Constants.EDHOC_MESSAGE_3, false).
                    get(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_ERROR)).get(0).
                    get(Integer.valueOf(Constants.SIDE_PROCESSOR_INNER_ERROR_DESCRIPTION)).AsString());
                int responseCodeValue = sideProcessor.getResults(Constants.EDHOC_MESSAGE_3, false).
                    get(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_ERROR)).get(0).
                    get(Integer.valueOf(Constants.SIDE_PROCESSOR_INNER_ERROR_RESP_CODE)).AsInt32();
                responseCode = ResponseCode.valueOf(responseCodeValue);
                error = true;
                    
                // No need to keep this information any longer in the side processor object
                sideProcessor.removeResultSet(Constants.EDHOC_MESSAGE_3, Constants.SIDE_PROCESSOR_OUTER_ERROR, false);
            }
        }
        CBORObject ead3[] = null;
        if (error == false) {
            List<CBORObject> myList = session.getSideProcessor().getProducedEADs(Constants.EDHOC_MESSAGE_3);

            // There are EAD items to include in EAD_3
            if (myList != null && myList.size() != 0) {
                ead3 = new CBORObject[myList.size()];
                for (int i = 0; i < myList.size(); i++) {
                    ead3[i] = myList.get(i);
                }
            }
        }
        
		
    	/* Start computing Signature_or_MAC_3 */
    	
    	// Compute MAC_3
        byte[] mac3 = null;
        
        if (error == false) {
	    	mac3 = computeMAC3(session, prk4e3m, th3, session.getIdCred(), session.getCred(), ead3);
	    	if (mac3 == null) {
	    		System.err.println("Error when computing MAC_3");
	    		errMsg = new String("Error when computing MAC_3");
	    		error = true;
	    	}
	    	else if (debugPrint) {
	    		Util.nicePrint("MAC_3", mac3);
	    	}
        }
    	
    	
    	// Compute Signature_or_MAC_3
    	
        // Compute the external data for the external_aad, as a CBOR sequence
        byte[] externalData = null;
        if (error == false) {
	    	externalData = computeExternalData(th3, session.getCred(), ead3);
	    	if (externalData == null) {
	    		System.err.println("Error when computing the external data for MAC_3");
	    		errMsg = new String("Error when computing the external data for MAC_3");
	    		error = true;
	    	}
        }
    	
        byte[] signatureOrMac3 = null;
        if (error == false) {
	    	signatureOrMac3 = computeSignatureOrMac3(session, mac3, externalData);
	    	if (signatureOrMac3 == null) {
	    		System.err.println("Error when computing Signature_or_MAC_3");
	    		errMsg = new String("Error when computing Signature_or_MAC_3");
	    		error = true;
	    	}
	    	else if (debugPrint) {
	    		Util.nicePrint("Signature_or_MAC_3", signatureOrMac3);
	    	}
        }
    	
    	/* End computing Signature_or_MAC_3 */
    	
    	
    	/* Start computing CIPHERTEXT_3 */
    	
    	// Compute K_3 and IV_3 to protect the outer COSE object

        byte[] k3 = null;
        if (error == false) {
	    	k3 = computeKey(Constants.EDHOC_K_3, session);
	    	if (k3 == null) {
	    		System.err.println("Error when computing K_3");
	    		errMsg = new String("Error when computing K_3");
	    		error = true;
	    	}
	    	else if (debugPrint) {
	    		Util.nicePrint("K_3", k3);
	    	}
        }
    	
        byte[] iv3 = null;
        if (error == false) {
	    	iv3 = computeIV(Constants.EDHOC_IV_3, session);
	    	if (iv3 == null) {
	    		System.err.println("Error when computing IV_3");
	    		errMsg = new String("Error when computing IV_3");
	    		error = true;
	    	}
	    	else if (debugPrint) {
	    		Util.nicePrint("IV_3", iv3);
	    	}
        }
    	    	
        
        if (error == false) {
	    	// Prepare the External Data as including only TH3
	    	externalData = th3;
	    	
	    	// Prepare the plaintext
	    	List<CBORObject> plaintextElementList = new ArrayList<>();
	    	CBORObject plaintextElement = null;
	    	if (session.getIdCred().ContainsKey(HeaderKeys.KID.AsCBOR())) {
	    	    // ID_CRED_I uses 'kid', whose value is the only thing to include in the plaintext
	    		CBORObject kid = session.getIdCred().get(HeaderKeys.KID.AsCBOR());
	    		plaintextElement = MessageProcessor.encodeIdentifier(kid.GetByteString());
	    	}
	    	else {
	    	    plaintextElement = session.getIdCred();
	    	}    	
	    	plaintextElementList.add(plaintextElement);
	    	plaintextElementList.add(CBORObject.FromObject(signatureOrMac3));
	    	if (ead3 != null) {
	    		for (int i = 0; i < ead3.length; i++)
	    			plaintextElementList.add(ead3[i]);
	    	}    	
	    	byte[] plaintext3 = Util.buildCBORSequence(plaintextElementList);
	    	if (debugPrint && plaintext3 != null) {
	    		Util.nicePrint("Plaintext to compute CIPHERTEXT_3", plaintext3);
	    	}
	    	
	    	
	    	// Compute CIPHERTEXT_3 and add it to the outer CBOR sequence
	    	
	    	byte[] ciphertext3 = computeCiphertext3(session, externalData, plaintext3, k3, iv3);
	    	objectList.add(CBORObject.FromObject(ciphertext3));
	    	if (debugPrint && ciphertext3 != null) {
	    		Util.nicePrint("CIPHERTEXT_3", ciphertext3);
	    	}
	    	
	    	/* End computing CIPHERTEXT_3 */
	    	
	    	
	    	/* Compute TH4 */
	    	
	        byte[] th3SerializedCBOR = CBORObject.FromObject(th3).EncodeToBytes();
	        
	        byte[] credI = session.getCred();
	        byte[] th4 = computeTH4(session, th3SerializedCBOR, plaintext3, credI);
	        
	        if (th4 == null) {
	        	System.err.println("Error when computing TH_4");
	    		errMsg = new String("Error when computing TH_4");
	    		error = true;
	        }
	        else if (debugPrint) {
	    		Util.nicePrint("TH_4", th4);
	    	}
	    	session.setTH4(th4);
		}

    	/* Compute PRK_out */
        byte[] prkOut = null;
        if (error == false) {
	    	prkOut = computePRKout(session, prk4e3m);
	        if (prkOut == null) {
	        	System.err.println("Error when computing PRK_out");
	    		errMsg = new String("Error when computing PRK_out");
	    		error = true;
	        }
	        else if (debugPrint) {
	    		Util.nicePrint("PRK_out", prkOut);
	    	}
	    	session.setPRKout(prkOut);
        }

    	/* Compute PRK_exporter */
        byte[] prkExporter = null;
        if (error == false) {
	    	prkExporter = computePRKexporter(session, prkOut);
	        if (prkExporter == null) {
	        	System.err.println("Error when computing PRK_exporter");
	    		errMsg = new String("Error when computing PRK_exporter");
	    		error = true;
	        }
	        else if (debugPrint) {
	    		Util.nicePrint("PRK_exporter", prkExporter);
	    	}
        }        
    	
        if (error == false) {
	        session.setPRKexporter(prkExporter);
	    	
	    	session.setCurrentStep(Constants.EDHOC_AFTER_M3);
	    	
	    	/* Delete ephemeral keys and other temporary material */
	    	session.deleteTemporaryMaterial();
        }
    	
    	
    	/* Prepare an EDHOC Error Message */

    	if (error == true) {
			byte[] connectionIdentifierResponder = session.getPeerConnectionId();
    	    
    	    List<CBORObject> processingResult = processError(errorCode, Constants.EDHOC_MESSAGE_2,
    	    												 session.isClientInitiated(),
    	    												 connectionIdentifierResponder,
    	    												 errMsg, null, responseCode);
    	    return processingResult.get(0).GetByteString();
    	}
    	
    	
    	/* Prepare EDHOC Message 3 */
    	
		if (debugPrint) {
		    byte[] sequenceBytesToPrint;
		    byte[] sequenceBytes = Util.buildCBORSequence(objectList);
		    
		    // If EDHOC message_3 is transported in a CoAP request, do not print the prepended C_R
		    if (session.isClientInitiated() == true) {
		    	List<CBORObject> trimmedSequence = new ArrayList<CBORObject>();
		    	for (int i = 1; i < objectList.size(); i++) {
		    		trimmedSequence.add(objectList.get(i));
		    	}
		        sequenceBytesToPrint = Util.buildCBORSequence(trimmedSequence);
		    }
		    else {
		        sequenceBytesToPrint = sequenceBytes;
		    }
		    Util.nicePrint("EDHOC Message 3", sequenceBytesToPrint);
		}
    	
        return Util.buildCBORSequence(objectList);
		
	}
	
	
    /**
     *  Write an EDHOC Message 4
     * @param session   The EDHOC session associated to this EDHOC message
     * @return  The raw payload to transmit as EDHOC Message 4 or EDHOC Error Message; or null in case of errors
     */
	public static byte[] writeMessage4(EdhocSession session) {
		
		List<CBORObject> objectList = new ArrayList<>();
		boolean error = false; // Will be set to True if an EDHOC Error Message has to be returned
		int errorCode = Constants.ERR_CODE_UNSPECIFIED_ERROR; // The error code to use for the EDHOC Error Message
		ResponseCode responseCode = ResponseCode.INTERNAL_SERVER_ERROR; // The CoAP response code to use for the EDHOC Error Message
		String errMsg = null; // The text string to be possibly returned as DIAG_MSG in an EDHOC Error Message
		
        if (debugPrint) {
        	System.out.println("===================================");
        	System.out.println("Start processing EDHOC Message 4:\n");
        }
		
        /* Start preparing data_4 */
		
        // C_I, if EDHOC message_4 is transported in a CoAP request
		if (!session.isClientInitiated()) {
			byte[] connectionIdentifierInitiator = session.getPeerConnectionId(); 
			CBORObject cI = encodeIdentifier(connectionIdentifierInitiator);
			objectList.add(cI);
	        if (debugPrint) {
	        	Util.nicePrint("Connection Identifier of the Initiator", connectionIdentifierInitiator);
	        	Util.nicePrint("C_I", cI.EncodeToBytes());
	        }
		}
		
		
		/* End preparing data_4 */
		
		
		/* Start computing the COSE object */
		
        // Compute the external data for the external_aad
		
		// Prepare the External Data as including only TH4
    	byte[] externalData = session.getTH4();

    	if (externalData == null) {
    		System.err.println("Error when computing the external data for CIPHERTEXT_4");
    		errMsg = new String("Error when computing the external data for CIPHERTEXT_4");
    		error = true;
    	}
    	else if (debugPrint) {
    		Util.nicePrint("External Data to compute CIPHERTEXT_4", externalData);
    	}
    	
    	
    	// Produce possible EAD items following early instructions from the application
    	if (error == false) {
    	    SideProcessor sideProcessor = session.getSideProcessor();
    	    
    	    sideProcessor.produceIndependentEADs(Constants.EDHOC_MESSAGE_4);
    	    
    	    // A fatal error occurred
    	    if (sideProcessor.getResults(Constants.EDHOC_MESSAGE_4, false).
    	            containsKey(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_ERROR))) {
    	        errMsg = new String(sideProcessor.getResults(Constants.EDHOC_MESSAGE_4, false).
    	            get(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_ERROR)).get(0).
    	            get(Integer.valueOf(Constants.SIDE_PROCESSOR_INNER_ERROR_DESCRIPTION)).AsString());
    	        int responseCodeValue = sideProcessor.getResults(Constants.EDHOC_MESSAGE_4, false).
    	            get(Integer.valueOf(Constants.SIDE_PROCESSOR_OUTER_ERROR)).get(0).
    	            get(Integer.valueOf(Constants.SIDE_PROCESSOR_INNER_ERROR_RESP_CODE)).AsInt32();
    	        responseCode = ResponseCode.valueOf(responseCodeValue);
    	        error = true;
    	            
    	        // No need to keep this information any longer in the side processor object
    	        sideProcessor.removeResultSet(Constants.EDHOC_MESSAGE_4, Constants.SIDE_PROCESSOR_OUTER_ERROR, false);
    	    }
    	}
    	CBORObject ead4[] = null;
    	if (error == false) {
    	    List<CBORObject> myList = session.getSideProcessor().getProducedEADs(Constants.EDHOC_MESSAGE_4);

    	    // There are EAD items to include in EAD_4
    	    if (myList != null && myList.size() != 0) {
    	        ead4 = new CBORObject[myList.size()];
    	        for (int i = 0; i < myList.size(); i++) {
    	            ead4[i] = myList.get(i);
    	        }
    	    }
    	}
    	
    	
        // Prepare the plaintext
    	byte[] plaintext4 = new byte[] {};
    	if (error == false) {
	    	if (ead4 != null) {
	    		List<CBORObject> plaintextElementList = new ArrayList<>();
	    	    for (int i = 0; i < ead4.length; i++) {
	    	        plaintextElementList.add(ead4[i]);
	    	    }
	    	    	plaintext4 = Util.buildCBORSequence(plaintextElementList);
	    	}
	    	if (debugPrint && error == false && plaintext4 != null) {
		        Util.nicePrint("Plaintext to compute CIPHERTEXT_4", plaintext4);
		    }
    	}
    	
    	
        // Compute the key material
      
    	// Compute K and IV to protect the COSE object
    	
    	byte[] k4 = null;
    	if (error == false) {    	
			k4 = computeKey(Constants.EDHOC_K_4, session);
			if (k4 == null) {
				System.err.println("Error when computing K_4");
				errMsg = new String("Error when computing K_4");
				error = true;
			}
			else if (debugPrint) {
				Util.nicePrint("K_4", k4);
			}
    	}

    	byte[] iv4 = null;
    	if (error == false) {
	    	iv4 = computeIV(Constants.EDHOC_IV_4, session);
	    	if (iv4 == null) {
	    		System.err.println("Error when computing IV_4");
	    		errMsg = new String("Error when computing IV_4");
	    		error = true;
	    	}
	    	else if (debugPrint) {
	    		Util.nicePrint("IV_4", iv4);
	    	}
    	}
    	
		
    	// Encrypt the COSE object and take the ciphertext as CIPHERTEXT_4
    	byte[] ciphertext4 = null;
    	if (error == false) {
	    	ciphertext4 = computeCiphertext4(session, externalData, plaintext4, k4, iv4);
	    	if (ciphertext4 == null) {
	    		System.err.println("Error when computing CIPHERTEXT_4");
	    		errMsg = new String("Error when computing CIPHERTEXT_4");
	    		error = true;
	    	}
	    	else if (debugPrint) {
	    		Util.nicePrint("CIPHERTEXT_4", ciphertext4);
	    	}
    	}
    	
        /* End computing the inner COSE object */

    	
    	/* Prepare an EDHOC Error Message */

    	if (error == true) {
    	    byte[] connectionIdentifierInitiator = session.getPeerConnectionId();
    	    
    	    List<CBORObject> processingResult = processError(errorCode, Constants.EDHOC_MESSAGE_3,
    	    												 !session.isClientInitiated(),
    	    												 connectionIdentifierInitiator,
    	    												 errMsg, null, responseCode);
    	    return processingResult.get(0).GetByteString();
    	}
    	
    	
    	session.setPRK4e3m(null);
    	session.setTH4(null);
    	
    	/* Prepare EDHOC Message 4 */
    	
    	objectList.add(CBORObject.FromObject(ciphertext4));
    	
    	if (debugPrint) {
    	    byte[] sequenceBytesToPrint;
    	    byte[] sequenceBytes = Util.buildCBORSequence(objectList);
    	    
    	    // If EDHOC message_2 is transported in a CoAP request, do not print the prepended C_I
    	    if (session.isClientInitiated() == false) {
    	        List<CBORObject> trimmedSequence = new ArrayList<CBORObject>();
    	        for (int i = 1; i < objectList.size(); i++) {
    	            trimmedSequence.add(objectList.get(i));
    	        }
    	        sequenceBytesToPrint = Util.buildCBORSequence(trimmedSequence);
    	    }
    	    else {
    	        sequenceBytesToPrint = sequenceBytes;
    	    }
    	    Util.nicePrint("EDHOC Message 4", sequenceBytesToPrint);
    	}
    	
    	session.setCurrentStep(Constants.EDHOC_AFTER_M4);
    	
        return Util.buildCBORSequence(objectList);
		
	}

	
    /**
     *  Write an EDHOC Error Message
     * @param errorCode   The error code for the EDHOC Error Message
     * @param replyTo   The message to which this EDHOC Error Message is intended to reply to
     * @param isErrorReq   True if the EDHOC Error Message will be sent in a CoAP request, or False otherwise
     * @param connectionIdentifier   The connection identifier of the intended recipient of the EDHOC Error Message, it can be null
     * @param errMsg   The text string to include in the EDHOC Error Message
     * @param suitesR   The cipher suite(s) supported by the Responder, it can be null;
     *                  (MUST be present in response to EDHOC Message 1)
     * @return  The raw payload to transmit as EDHOC Error Message, or null in case of errors
     */
	public static byte[] writeErrorMessage(int errorCode, int replyTo, boolean isErrorReq,
			                               byte[] connectionIdentifier, String errMsg, CBORObject suitesR) {
		
		if (replyTo != Constants.EDHOC_MESSAGE_1 && replyTo != Constants.EDHOC_MESSAGE_2 &&
			replyTo != Constants.EDHOC_MESSAGE_3 && replyTo != Constants.EDHOC_MESSAGE_4) {
				   return null;
		}
		
		if (suitesR != null && suitesR.getType() != CBORType.Integer && suitesR.getType() != CBORType.Array)
			return null;
		
		if (suitesR != null && suitesR.getType() == CBORType.Array) {
			for (int i = 0 ; i < suitesR.size(); i++) {
				if (suitesR.get(i).getType() != CBORType.Integer)
					return null;
			}
		}
		
		List<CBORObject> objectList = new ArrayList<CBORObject>();
		byte[] payload;
			
		// Possibly include C_X - This might not have been included if the incoming EDHOC message was malformed
		if (connectionIdentifier != null && isErrorReq == true) {
				CBORObject cX = encodeIdentifier(connectionIdentifier);
				objectList.add(cX);
		}

		// Include ERR_CODE
		objectList.add(CBORObject.FromObject(errorCode));
		
		// Include ERR_INFO
		if (errorCode == Constants.ERR_CODE_UNSPECIFIED_ERROR) {
			if (errMsg == null)
				return null;
			
			// Include DIAG_MSG
			objectList.add(CBORObject.FromObject(errMsg));
		}
		else if (errorCode == Constants.ERR_CODE_WRONG_SELECTED_CIPHER_SUITE) {
			if (replyTo != Constants.EDHOC_MESSAGE_1)
				return null;
			
			// Possibly include SUITES_R - This implies that EDHOC Message 1 was good enough and yielding a suite negotiation
			if (suitesR != null)
				objectList.add(suitesR);
		}
		
		
		// Encode the EDHOC Error Message, as a CBOR sequence
		payload = Util.buildCBORSequence(objectList);
		
		System.out.println("Completed preparation of EDHOC Error Message");
		
		return payload;
		
	}
	
	
    /**
     *  Prepare a list of CBOR objects to return, anticipating the sending of an EDHOC Error Message
     * @param errorCode   The error code for the EDHOC Error Message
     * @param replyTo   The message to which this EDHOC Error Message is intended to reply to
     * @param isErrorReq   True if the EDHOC Error Message will be sent in a CoAP request, or False otherwise
     * @param connectionIdentifier   The connection identifier of the intended recipient of the EDHOC Error Message, it can be null
     * @param errMsg   The text string to include in the EDHOC Error Message
     * @param suitesR   The cipher suite(s) supported by the Responder (only in response to EDHOC Message 1), it can be null
     * @return  A list of CBOR objects, including the EDHOC Error Message and the CoAP response code to use
     */
	public static List<CBORObject> processError(int errorCode, int replyTo, boolean isErrorReq, byte[] connectionIdentifier,
			                                    String errMsg, CBORObject suitesR, ResponseCode responseCode) {
		
		List<CBORObject> processingResult = new ArrayList<CBORObject>(); // List of CBOR Objects to return as result
		
		byte[] replyPayload = writeErrorMessage(errorCode, replyTo, isErrorReq, connectionIdentifier, errMsg, suitesR);
		
		// EDHOC Error Message, as a CBOR byte string
		processingResult.add(CBORObject.FromObject(replyPayload));
		
		// CoAP response code as a CBOR integer
		processingResult.add(CBORObject.FromObject(responseCode.value));
		
		if (errMsg != null) {
			System.err.println(errMsg);
		}
		
		return processingResult;
		
	}

    /**
     *  Create a new EDHOC session as an Initiator
     * @param method   The authentication method signaled by the Initiator
     * @param keyPairs   The key pairs of the Initiator (one per supported curve)
     * @param idCreds   The identifiers of the authentication credentials of the Initiator
     * @param creds    The authentication credentials of the Initiator (one per supported curve),
     *                 as the serialization of a CBOR object
     * @param supportedCipherSuites   The list of cipher suites supported by the Initiator
     * @param peerSupportedCipherSuites   The list of cipher suites supported by the Responder, as far as the Initiator knows
     * @param supportedEADs   The set of EAD items supported by the Responder
     * @param usedConnectionIds   The set of allocated Connection Identifiers for the Initiator.
     *                            Each identifier is wrapped in a CBOR byte string.
     * @param appProfile   The application profile used for this session
     * @param trustModel   The trust model used for validating authentication credentials of other peers
     * @param db   The database of OSCORE Security Contexts
     * @return  The newly created EDHOC session
     */
	public static EdhocSession createSessionAsInitiator(int method, HashMap<Integer, HashMap<Integer, OneKey>> keyPairs,
														HashMap<Integer, HashMap<Integer, CBORObject>> idCreds,
														HashMap<Integer, HashMap<Integer, CBORObject>> creds,
			  									        List<Integer> supportedCipherSuites,
			  									        List<Integer> peerSupportedCipherSuites,
			  									        Set<Integer> supportedEADs,
			  									        HashMap<Integer, List<CBORObject>> eadProductionInput,
			  									        Set<CBORObject> usedConnectionIds,
			  									        AppProfile appProfile, int trustModel, HashMapCtxDB db) {
		
		byte[] connectionId = null;
		HashMapCtxDB oscoreDB = (appProfile.getUsedForOSCORE() == true) ? db : null;
		
		connectionId = Util.getConnectionId(usedConnectionIds, oscoreDB, null);
		// Forced for testing
		// connectionId = new byte[] {(byte) 0x1c};
		
        EdhocSession mySession = new EdhocSession(true, true, method, connectionId, keyPairs, idCreds, creds,
        										  supportedCipherSuites, peerSupportedCipherSuites, supportedEADs,
        										  appProfile, trustModel, oscoreDB);
    
		return mySession;
		
	}
	
    /**
     *  Create a new EDHOC session as a Responder
     * @param message1   The payload of the received EDHOC Message 1
     * @param keyPairs   The key pairs of the Responder (one per supported curve)
     * @param idCreds   The identifiers of the authentication credentials of the Responder
     * @param creds    The authentication credentials of the Responder (one per supported curve), as the serialization of a CBOR object
     * @param supportedCipherSuites   The list of cipher suites supported by the Responder
     * @param supportedEADs   The set of EAD items supported by the Responder
     * @param usedConnectionIds   The set of allocated Connection Identifiers for the Responder
     * @param appProfile   The application profile used for this session
     * @param trustModel   The trust model used for validating authentication credentials of other peers
     * @param db   The database of OSCORE Security Contexts
     * @return  The newly created EDHOC session
     */
	public static EdhocSession createSessionAsResponder(byte[] message1, boolean isReq,
														HashMap<Integer, HashMap<Integer, OneKey>> keyPairs,
														HashMap<Integer, HashMap<Integer, CBORObject>> idCreds,
														HashMap<Integer, HashMap<Integer, CBORObject>> creds,
			  									        List<Integer> supportedCipherSuites,
			  									        Set<Integer> supportedEADs,
			  									        Set<CBORObject> usedConnectionIds,
			  									        AppProfile appProfile, int trustModel, HashMapCtxDB db) {
		
		CBORObject[] objectListMessage1 = CBORObject.DecodeSequenceFromBytes(message1);
		int index = -1;
		
		// Retrieve elements from EDHOC Message 1
		
	    // If the received message is a request (i.e. the CoAP client is the initiator), the first element
	    // before the actual message_1 is the CBOR simple value 'true', i.e. the byte 0xf5, and it must be skipped
	    if (isReq == true) {
	        index++;
	    }
		
		// METHOD
	    index++;
		int method = objectListMessage1[index].AsInt32();
		
		// Selected cipher suites from SUITES_I
		index++;
		int selectedCipherSuite = -1;
		if (objectListMessage1[index].getType() == CBORType.Integer)
			selectedCipherSuite = objectListMessage1[index].AsInt32();
		else if (objectListMessage1[index].getType() == CBORType.Array)
			selectedCipherSuite = objectListMessage1[index].get(0).AsInt32();
		
		// G_X
		index++;
		byte[] gX = objectListMessage1[index].GetByteString();
		
		// C_I
		index++;
		// Previous checks have ensured that C_I has a valid encoding 
		CBORObject cI = objectListMessage1[index];
		byte[] connectionIdentifierInitiator = decodeIdentifier(cI);
		
		// Create a new EDHOC session

		byte[] connectionIdentifierResponder = null;
		
		HashMapCtxDB oscoreDB = (appProfile.getUsedForOSCORE() == true) ? db : null;
		
		connectionIdentifierResponder = Util.getConnectionId(usedConnectionIds, oscoreDB, connectionIdentifierInitiator);
		// Forced for testing
		// connectionIdentifierResponder = new byte[] {(byte) 0x01};
		
		List<Integer> peerSupportedCipherSuites = new ArrayList<Integer>();
		EdhocSession mySession = new EdhocSession(false, isReq, method, connectionIdentifierResponder, keyPairs,
												  idCreds, creds, supportedCipherSuites, peerSupportedCipherSuites,
												  supportedEADs, appProfile, trustModel, oscoreDB);
		
		// Set the selected cipher suite
		mySession.setSelectedCipherSuite(selectedCipherSuite);
		
		// Set the asymmetric key pair, CRED and ID_CRED of the Responder to use in this session
		mySession.setAuthenticationCredential();
		
		// Set the Connection Identifier of the peer
		mySession.setPeerConnectionId(connectionIdentifierInitiator);
		
		// Set the ephemeral public key of the Initiator
		OneKey peerEphemeralKey = null;
		
		if (selectedCipherSuite == Constants.EDHOC_CIPHER_SUITE_0 || selectedCipherSuite == Constants.EDHOC_CIPHER_SUITE_1) {
			peerEphemeralKey = SharedSecretCalculation.buildCurve25519OneKey(null, gX);
		}
		if (selectedCipherSuite == Constants.EDHOC_CIPHER_SUITE_2 || selectedCipherSuite == Constants.EDHOC_CIPHER_SUITE_3) {
			peerEphemeralKey = SharedSecretCalculation.buildEcdsa256OneKey(null, gX, null);
		}
		mySession.setPeerEphemeralPublicKey(peerEphemeralKey);
				
		// Compute and store the hash of EDHOC Message 1
		// If it came as a CoAP request, the first received byte 0xf5 must be skipped
		int offset = (isReq == true) ? 1 : 0;
		byte[] hashInput = new byte[message1.length - offset];
		System.arraycopy(message1, offset, hashInput, 0, hashInput.length);
		mySession.setHashMessage1(hashInput);
		
		return mySession;
		
	}
	
	
	/**
    *  Compute one of the temporary keys
    * @param keyName   The name of the key to compute
    * @param session   The used EDHOC session
    * @return  The computed key
    */
	public static byte[] computeKey(int keyName, EdhocSession session) {
	    
	    int selectedCipherSuite = session.getSelectedCipherSuite();
	    int keyLength = EdhocSession.getKeyLengthEdhocAEAD(selectedCipherSuite);
	    if (keyLength == 0)
	        return null;
	    
	    byte[] key = null;
	    String output = null;
	    CBORObject context = null;
	    
	    try {
	        switch(keyName) {
	            case Constants.EDHOC_K_3:
	            	
	            	output = new String("K_3");
	            	byte[] th3 = session.getTH3();
	            	context = CBORObject.FromObject(th3);
	                key = session.edhocKDF(session.getPRK3e2m(), Constants.KDF_LABEL_K_3, context, keyLength);
	                
	                break;
	            case Constants.EDHOC_K_4:
	            	
	            	output = new String("K_4");
	                byte[] th4 = session.getTH4();
	            	context = CBORObject.FromObject(th4);
	                key = session.edhocKDF(session.getPRK4e3m(), Constants.KDF_LABEL_K_4, context, keyLength);
	                
	                break;
	            default:
	            	key = null;
	            	break;
	        }
	    } catch (InvalidKeyException e) {
	        System.err.println("Error when generating " + output + "\n" + e.getMessage());
	    } catch (NoSuchAlgorithmException e) {
	        System.err.println("Error when generating " + output + "\n" + e.getMessage());
	    }
	    
	    return key;
	    
	}
	
	
	/**
    *  Compute one of the temporary IVs
    * @param ivName   The name of the IV to compute
    * @param session   The used EDHOC session
    * @return  The computed IV
    */
	public static byte[] computeIV(int ivName, EdhocSession session) {
	    
	    int selectedCipherSuite = session.getSelectedCipherSuite();
	    int ivLength = EdhocSession.getIvLengthEdhocAEAD(selectedCipherSuite);
	    if (ivLength == 0)
	        return null;
	    
	    byte[] iv = null;
	    String output = null;
	    CBORObject context = null;
	    
	    try {
	        switch(ivName) {
            case Constants.EDHOC_IV_3:
            	output = new String("IV_3");
            	byte[] th3 = session.getTH3();
            	context = CBORObject.FromObject(th3);
                iv = session.edhocKDF(session.getPRK3e2m(), Constants.KDF_LABEL_IV_3, context, ivLength);
                break;
            case Constants.EDHOC_IV_4:
            	output = new String("IV_4");
                byte[] th4 = session.getTH4();
            	context = CBORObject.FromObject(th4);
                iv = session.edhocKDF(session.getPRK4e3m(), Constants.KDF_LABEL_IV_4, context, ivLength);
                break;
            default:
            	iv = null;
            	break;
	        }
	    } catch (InvalidKeyException e) {
	    	System.err.println("Error when generating " + output + "\n" + e.getMessage());
	        return null;
	    } catch (NoSuchAlgorithmException e) {
	    	System.err.println("Error when generating " + output + "\n" + e.getMessage());
	        return null;
	    }
	    
	    return iv;
	    
	}

    /**
     *  Compute the keystream KEYSTREAM_2
     * @param session   The used EDHOC session
     * @param length   The desired length in bytes for the keystream KEYSTREAM_2
     * @return  The computed keystream KEYSTREAM_2
     */
	public static byte[] computeKeystream2(EdhocSession session, int length) {
    	
		byte[] keystream2 = null;
		
		byte[] prk2e = session.getPRK2e();
		
		CBORObject context = CBORObject.FromObject(session.getTH2());
		
    	int selectedCipherSuite = session.getSelectedCipherSuite();
    	String hashAlg = EdhocSession.getEdhocHashAlg(selectedCipherSuite);
    	int hashLength = EdhocSession.getEdhocHashAlgOutputSize(selectedCipherSuite);
		
    	if ( (!hashAlg.equals("SHA-256") && !hashAlg.equals("SHA-384") && !hashAlg.equals("SHA-512")) || (length <= 255 * hashLength) ) {
			try {
				keystream2 = session.edhocKDF(prk2e, Constants.KDF_LABEL_KEYSTREAM_2, context, length);
			} catch (InvalidKeyException e) {
				System.err.println("Error when generating KEYSTREAM_2\n" + e.getMessage());
				return null;
			} catch (NoSuchAlgorithmException e) {
				System.err.println("Error when generating KEYSTREAM_2\n" + e.getMessage());
				return null;
			}
    	}
    	else {
			byte[] part = null;
    		int regularPartSize = 255 * hashLength;
    		int lastPartSize = regularPartSize;
    		
    		int numParts = length / (regularPartSize);
    		if ((length % (regularPartSize)) != 0) {
    			lastPartSize = length % (regularPartSize);
    			numParts++;
    		}
    		
    		int offset = 0;
    		keystream2 = new byte[length];
    		for (int i = 0; i < numParts; i++) {
    			int numBytes = (i == (numParts - 1)) ? lastPartSize : regularPartSize;
    			
    			try {
    				part = session.edhocKDF(prk2e, -i, context, numBytes);
    			} catch (InvalidKeyException e) {
    				System.err.println("Error when generating KEYSTREAM_2\n" + e.getMessage());
    				return null;
    			} catch (NoSuchAlgorithmException e) {
    				System.err.println("Error when generating KEYSTREAM_2\n" + e.getMessage());
    				return null;
    			}
    			
    			System.arraycopy(part, 0, keystream2, offset, part.length);
    			offset += part.length;
    		}
    	}

 
		return keystream2;
		
	}


    /**
     *  Compute the key PRK_2e
     * @param th2   The transcript hash TH_2
     * @param dhSecret   The Diffie-Hellman secret
     * @param hashAlgorithm   The EDHOC hash algorithm of the selected cipher suite
     * @return  The computed key PRK_2e
     */
	public static byte[] computePRK2e(byte[] th2, byte[] dhSecret, String hashAlgorithm) {
	
		byte[] prk2e = null;
	    try {  	
	    	if (hashAlgorithm.equals("SHA-256") || hashAlgorithm.equals("SHA-384") || hashAlgorithm.equals("SHA-512")) {
	    		prk2e = Hkdf.extract(th2, dhSecret);
	    	}
		} catch (InvalidKeyException e) {
			System.err.println("Error when generating PRK_2e\n" + e.getMessage());
			return null;
		} catch (NoSuchAlgorithmException e) {
			System.err.println("Error when generating PRK_2e\n" + e.getMessage());
			return null;
		}
	    
	    return prk2e;
		
	}

    /**
     *  Compute the key PRK_3e2m
     * @param session   The used EDHOC session
     * @param prk2e   The key PRK_2e
     * @return  The computed key PRK_3e2m
     */
	public static byte[] computePRK3e2m(EdhocSession session, byte[] prk2e) {
		
		byte[] prk3e2m = null;
		int authenticationMethod = session.getMethod();
		
        if (authenticationMethod == Constants.EDHOC_AUTH_METHOD_0 || authenticationMethod == Constants.EDHOC_AUTH_METHOD_2) {
        	// The responder uses signatures as authentication method, then PRK_3e2m is equal to PRK_2e 
        	prk3e2m = new byte[prk2e.length];
        	System.arraycopy(prk2e, 0, prk3e2m, 0, prk2e.length);
        }
        else if (authenticationMethod == Constants.EDHOC_AUTH_METHOD_1 || authenticationMethod == Constants.EDHOC_AUTH_METHOD_3) {
        		// The responder does not use signatures as authentication method, then PRK_3e2m has to be computed
            	byte[] dhSecret;
            	OneKey privateKey = null;
            	OneKey publicKey = null;
            	
            	if (session.isInitiator() == false) {
            		// Use the ephemeral key of the Initiator as public key
            		publicKey = session.getPeerEphemeralPublicKey();
            		
            		// Use the long-term key of the Responder as private key
            		privateKey = session.getKeyPair();

                	// For the time being, this is not relevant, since the same
                	// key pair cannot be used for both signing and Diffie-Hellman.
            		/*
                	OneKey identityKey = session.getKeyPair();
            		
	            	if (identityKey.get(KeyKeys.OKP_Curve).AsInt32() == KeyKeys.OKP_Ed25519.AsInt32()) {
	                	// Convert the identity key from Edward to Montgomery form
	                	try {
	                		privateKey = SharedSecretCalculation.convertEd25519ToCurve25519(identityKey);
						} catch (CoseException e) {
							System.err.println("Error when converting the Responder identity key" + 
									           "from Edward to Montgomery format\n" + e.getMessage());
							return null;
						}
	            	}
	            	else {
	            		privateKey = identityKey;
	            	}
	            	*/
            	}
            	else if (session.isInitiator() == true) {
            		// Use the ephemeral key of the Initiator as private key
            		privateKey = session.getEphemeralKey();
            		
            		// Use the long-term key of the Responder as public key
            		publicKey = session.getPeerLongTermPublicKey();

                	// For the time being, this is not relevant, since the same
                	// key pair cannot be used for both signing and Diffie-Hellman.
            		/*
            		/*
            		OneKey peerIdentityKey = session.getPeerLongTermPublicKey();
            		
	            	if (peerIdentityKey.get(KeyKeys.OKP_Curve).AsInt32() == KeyKeys.OKP_Ed25519.AsInt32()) {
	                	// Convert the identity key from Edward to Montgomery form
	                	try {
							publicKey = SharedSecretCalculation.convertEd25519ToCurve25519(peerIdentityKey);
						} catch (CoseException e) {
							System.err.println("Error when converting the Responder identity key" + 
									           "from Edward to Montgomery format\n" + e.getMessage());
							return null;
						}
	            	}
	            	else {
	            		publicKey = peerIdentityKey;
	            	}
	            	*/
	            	
            	}
            	
    			// Consistency check of key type and curve against the selected cipher suite
            	int selectedCipherSuite = session.getSelectedCipherSuite();
            	
            	if (Util.checkDiffieHellmanKeyAgainstCipherSuite(privateKey, selectedCipherSuite) == false) {
            		System.err.println("Error when computing the Diffie-Hellman Secret");
            		return null;
            	}
            	if (Util.checkDiffieHellmanKeyAgainstCipherSuite(publicKey, selectedCipherSuite) == false) {
            		System.err.println("Error when computing the Diffie-Hellman Secret");
            		return null;
            	}
            	
            	dhSecret = SharedSecretCalculation.generateSharedSecret(privateKey, publicKey);
            	
                if (dhSecret == null) {
            		System.err.println("Error when computing the Diffie-Hellman Secret");
            		return null;
                }
            	
            	if (debugPrint) {
            		Util.nicePrint("G_RX", dhSecret);
            	}
            	
            	String hashAlgorithm = EdhocSession.getEdhocHashAlg(session.getSelectedCipherSuite());

            	// Compute SALT_3e2m
            	byte[] salt3e2m = computeSALT3e2m(session, prk2e);
            	if (salt3e2m == null) {
            		System.err.println("Error when computing SALT_3e2m");
            		return null;
            	}
            	else if (debugPrint) {
            		Util.nicePrint("SALT_3e2m", salt3e2m);
            	}
            	
            	try {
            		if (hashAlgorithm.equals("SHA-256") || hashAlgorithm.equals("SHA-384") || hashAlgorithm.equals("SHA-512")) {
            			prk3e2m = Hkdf.extract(salt3e2m, dhSecret);
            		}
				} catch (InvalidKeyException e) {
					System.err.println("Error when generating PRK_3e2m\n" + e.getMessage());
					return null;
				} catch (NoSuchAlgorithmException e) {
					System.err.println("Error when generating PRK_3e2m\n" + e.getMessage());
					return null;
				}
            	finally {
            		dhSecret = null;
            	}
    	}
        
        return prk3e2m;
        
	}
	
    /**
     *  Compute the key PRK_4e3m
     * @param session   The used EDHOC session
     * @return  The computed key PRK_4e3m
     */
	public static byte[] computePRK4e3m(EdhocSession session) {
		
		byte[] prk4e3m = null;
		byte[] prk3e2m = session.getPRK3e2m();
		int authenticationMethod = session.getMethod();
		
        if (authenticationMethod == Constants.EDHOC_AUTH_METHOD_0 || authenticationMethod == Constants.EDHOC_AUTH_METHOD_1) {
        	// The initiator uses signatures as authentication method, then PRK_4e3m is equal to PRK_3e2m 
        	prk4e3m = new byte[prk3e2m.length];
        	System.arraycopy(prk3e2m, 0, prk4e3m, 0, prk3e2m.length);
        }
        else if (authenticationMethod == Constants.EDHOC_AUTH_METHOD_2 || authenticationMethod == Constants.EDHOC_AUTH_METHOD_3) {
        		// The initiator does not use signatures as authentication method, then PRK_4e3m has to be computed
            	byte[] dhSecret;
            	OneKey privateKey = null;
            	OneKey publicKey = null;
            	
            	if (session.isInitiator() == false) {
            		// Use the ephemeral key of the Responder as private key
                	privateKey = session.getEphemeralKey();
                	
            		// Use the long-term key of the Initiator as public key
                	publicKey = session.getPeerLongTermPublicKey();

                	// For the time being, this is not relevant, since the same
                	// key pair cannot be used for both signing and Diffie-Hellman.
                	/*
            		OneKey identityKey = session.getPeerLongTermPublicKey();
            		
	            	if (identityKey.get(KeyKeys.OKP_Curve).AsInt32() == KeyKeys.OKP_Ed25519.AsInt32()) {
	                	// Convert the identity key from Edward to Montgomery form
	                	try {
	                		publicKey = SharedSecretCalculation.convertEd25519ToCurve25519(identityKey);
						} catch (CoseException e) {
							System.err.println("Error when converting the Responder identity key" + 
									           "from Edward to Montgomery format\n" + e.getMessage());
							return null;
						}
	            	}
	            	else {
	            		publicKey = identityKey;
	            	}
	            	*/
            	}
            	else if (session.isInitiator() == true) {
            		// Use the ephemeral key of the Responder as public key
            		publicKey = session.getPeerEphemeralPublicKey();
            		
            		// Use the long-term key of the Initiator as private key
            		privateKey = session.getKeyPair();

                	// For the time being, this is not relevant, since the same
                	// key pair cannot be used for both signing and Diffie-Hellman.
            		/*
            		OneKey identityKey = session.getKeyPair();
            		
	            	if (identityKey.get(KeyKeys.OKP_Curve).AsInt32() == KeyKeys.OKP_Ed25519.AsInt32()) {
	                	// Convert the identity key from Edward to Montgomery form
	                	try {
							privateKey = SharedSecretCalculation.convertEd25519ToCurve25519(identityKey);
						} catch (CoseException e) {
							System.err.println("Error when converting the Responder identity key" + 
									           "from Edward to Montgomery format\n" + e.getMessage());
							return null;
						}
	            	}
	            	else {
	            		privateKey = identityKey;
	            	}
	            	*/
	            	
            	}
            	dhSecret = SharedSecretCalculation.generateSharedSecret(privateKey, publicKey);
            	
                if (dhSecret == null) {
            		System.err.println("Error when computing the Diffie-Hellman Secret");
            		return null;
                }
            	
            	if (debugPrint) {
            		Util.nicePrint("G_IY", dhSecret);
            	}
            	
            	String hashAlgorithm = EdhocSession.getEdhocHashAlg(session.getSelectedCipherSuite());

            	// Compute SALT_4e3m
            	byte[] salt4e3m = computeSALT4e3m(session, prk3e2m);
            	if (salt4e3m == null) {
            		System.err.println("Error when computing SALT_4e3m");
            		return null;
            	}
            	else if (debugPrint) {
            		Util.nicePrint("SALT_4e3m", salt4e3m);
            	}
            	
            	try {
            		if (hashAlgorithm.equals("SHA-256") || hashAlgorithm.equals("SHA-384") || hashAlgorithm.equals("SHA-512")) {
            			prk4e3m = Hkdf.extract(salt4e3m, dhSecret);
            		}
				} catch (InvalidKeyException e) {
					System.err.println("Error when generating PRK_4e3m\n" + e.getMessage());
					return null;
				} catch (NoSuchAlgorithmException e) {
					System.err.println("Error when generating PRK_4e3m\n" + e.getMessage());
					return null;
				}
            	finally {
            		dhSecret = null;
            	}
    	}
        
        return prk4e3m;
        
	}

    /**
     *  Compute the PRK_out
     * @param session   The used EDHOC session
     * @param prk4e3m   The key PRK_4e3m
     * @return  The computed PRK_out
     */
	public static byte[] computePRKout(EdhocSession session, byte[] prk4e3m) {
		
		byte[] prkOut = null;
		int selectedCipherSuite = session.getSelectedCipherSuite();
		int length = EdhocSession.getEdhocHashAlgOutputSize(selectedCipherSuite);
		CBORObject context = CBORObject.FromObject(session.getTH4());
		
		try {
			prkOut = session.edhocKDF(prk4e3m, Constants.KDF_LABEL_PRK_OUT, context, length);
		} catch (InvalidKeyException e) {
			System.err.println("Error when generating PRK_out\n" + e.getMessage());
			return null;
		} catch (NoSuchAlgorithmException e) {
			System.err.println("Error when generating PRK_out\n" + e.getMessage());
			return null;
		}

		return prkOut;
		
	}

    /**
     *  Compute the PRK_exporter
     * @param session   The used EDHOC session
     * @param prkOut   The key PRK_out
     * @return  The computed PRK_exporter
     */
	public static byte[] computePRKexporter(EdhocSession session, byte[] prkOut) {
		
		byte[] prkExporter = null;
		int selectedCipherSuite = session.getSelectedCipherSuite();
		int length = EdhocSession.getEdhocHashAlgOutputSize(selectedCipherSuite);
	    CBORObject context = CBORObject.FromObject(new byte[0]);
	    
		try {
			prkExporter = session.edhocKDF(prkOut, Constants.KDF_LABEL_PRK_EXPORTER, context, length);
		} catch (InvalidKeyException e) {
			System.err.println("Error when generating PRK_exporter\n" + e.getMessage());
			return null;
		} catch (NoSuchAlgorithmException e) {
			System.err.println("Error when generating PRK_exporter\n" + e.getMessage());
			return null;
		}

		return prkExporter;
		
	}

    /**
     *  Compute the SALT_3e2m
     * @param session   The used EDHOC session
     * @param prk2e   The key PRK_2e
     * @return  The computed SALT_3e2m
     */
	public static byte[] computeSALT3e2m(EdhocSession session, byte[] prk2e) {
		
		byte[] salt3e2m = null;
		int selectedCipherSuite = session.getSelectedCipherSuite();
		int length = EdhocSession.getEdhocHashAlgOutputSize(selectedCipherSuite);
		CBORObject context = CBORObject.FromObject(session.getTH2());
		
		try {
			salt3e2m = session.edhocKDF(prk2e, Constants.KDF_LABEL_SALT_3E2M, context, length);
		} catch (InvalidKeyException e) {
			System.err.println("Error when generating SALT_3e2m\n" + e.getMessage());
			return null;
		} catch (NoSuchAlgorithmException e) {
			System.err.println("Error when generating SALT_3e2m\n" + e.getMessage());
			return null;
		}

		return salt3e2m;
		
	}
	
    /**
     *  Compute the SALT_4e3m
     * @param session   The used EDHOC session
     * @param prk3e2m   The key PRK_3e2m
     * @return  The computed SALT_4e3m
     */
	public static byte[] computeSALT4e3m(EdhocSession session, byte[] prk3e2m) {
		
		byte[] salt4e3m = null;
		int selectedCipherSuite = session.getSelectedCipherSuite();
		int length = EdhocSession.getEdhocHashAlgOutputSize(selectedCipherSuite);
		CBORObject context = CBORObject.FromObject(session.getTH3());
		
		try {
			salt4e3m = session.edhocKDF(prk3e2m, Constants.KDF_LABEL_SALT_4E3M, context, length);
		} catch (InvalidKeyException e) {
			System.err.println("Error when generating SALT_4e3m\n" + e.getMessage());
			return null;
		} catch (NoSuchAlgorithmException e) {
			System.err.println("Error when generating SALT_4e3m\n" + e.getMessage());
			return null;
		}

		return salt4e3m;
		
	}
	
    /**
     *  Compute External_Data_2 / External_Data_3 for computing/verifying Signature_or_MAC_2 and Signature_or_MAC_3
     * @param th   The transcript hash TH2 or TH3
     * @param cred   The CRED of the long-term public key of the caller
     * @param ad   The array of CBOR objects composing the External Authorization Data, it can be null
     * @return  The external data for computing/verifying Signature_or_MAC_2 and Signature_or_MAC_3, or null in case of error
     */
	public static byte[] computeExternalData(byte[] th, byte[] cred, CBORObject[] ead) {
		
		if (th == null || cred == null)
			return null;
		
		List<CBORObject> externalDataList = new ArrayList<>();
		
        // TH2 / TH3 is the first element of the CBOR Sequence
        byte[] thSerializedCBOR = CBORObject.FromObject(th).EncodeToBytes();
        externalDataList.add(CBORObject.FromObject(thSerializedCBOR));
        
        // CRED_R / CRED_I is the second element of the CBOR Sequence
        byte[] credSerializedCBOR = cred;
        externalDataList.add(CBORObject.FromObject(credSerializedCBOR));
        
        // EAD_2 / EAD_3 is the third element of the CBOR Sequence (if provided)
        if (ead != null && ead.length != 0) {
        	byte[] eadSequence = null;
        	List<CBORObject> objectList = new ArrayList<CBORObject>();
        	
	    	for (int i = 0; i < ead.length; i++) {
		    	objectList.add(ead[i]);
		    }
	    	// Rebuild how EAD was in the EDHOC message
		    eadSequence = Util.buildCBORSequence(objectList);
    		    	
            externalDataList.add(CBORObject.FromObject(eadSequence)); 
        }
		
		return Util.concatenateByteArrays(externalDataList);
		
	}

    /**
     *  Compute MAC_2
     * @param session   The used EDHOC session
     * @param prk3e2m   The PRK used to compute MAC_2
     * @param th2   The transcript hash TH2
     * @param idCredR   The ID_CRED_R associated to the long-term public key of the Responder
     * @param credR   The long-term public key of the Responder, as the serialization of a CBOR object
     * @param ead2   The External Authorization Data from EDHOC Message 2, it can be null
     * @return  The computed MAC_2
     */
	public static byte[] computeMAC2(EdhocSession session, byte[] prk3e2m, byte[] th2,
			                         CBORObject idCredR, byte[] credR, CBORObject[] ead2) {
		
		// Build the CBOR sequence to use for 'context': ( ID_CRED_R, TH_2, CRED_R, ? EAD_2 )
		// The actual 'context' is a CBOR byte string with value the serialization of the CBOR sequence
        List<CBORObject> objectList = new ArrayList<>();
    	objectList.add(idCredR);
    	objectList.add(CBORObject.FromObject(th2));
    	objectList.add(CBORObject.DecodeFromBytes(credR));
    	
    	if (ead2 != null && ead2.length != 0) {
	    	for (int i = 0; i < ead2.length; i++)
	    		objectList.add(ead2[i]);
    	}
    	byte[] contextSequence = Util.buildCBORSequence(objectList);
    	CBORObject context = CBORObject.FromObject(contextSequence);
    	
    	int macLength = 0;
    	int method = session.getMethod();
    	int selectedCipherSuite = session.getSelectedCipherSuite();
    	if (method == 0 || method == 2) {
    		macLength = EdhocSession.getEdhocHashAlgOutputSize(selectedCipherSuite);
    	}
    	if (method == 1 || method == 3) {
    		macLength = EdhocSession.getTagLengthEdhocAEAD(selectedCipherSuite);
    	}
    	
    	byte[] mac2 = null;
    	
    	try {
			mac2 = session.edhocKDF(prk3e2m, Constants.KDF_LABEL_MAC_2, context, macLength);
		} catch (InvalidKeyException e) {
			System.err.println("Error when computing MAC_2\n" + e.getMessage());
		} catch (NoSuchAlgorithmException e) {
			System.err.println("Error when computing MAC_2\n" + e.getMessage());
		}
        if (debugPrint) {
        	Util.nicePrint("context_2", contextSequence);
        }
		
		return mac2;
		
	}

    /**
     *  Compute MAC_3
     * @param session   The used EDHOC session
     * @param prk4e3m   The PRK used to compute MAC_3
     * @param idCredI   The ID_CRED_I associated to the long-term public key of the Initiator
     * @param credI   The long-term public key of the Initiator, as the serialization of a CBOR object
     * @param ead3   The External Authorization Data from EDHOC Message 3, it can be null
     * @return  The computed MAC_3
     */
	public static byte[] computeMAC3(EdhocSession session, byte[] prk4e3m, byte[] th3,
            						 CBORObject idCredI, byte[] credI, CBORObject[] ead3) {
				
		// Build the CBOR sequence for 'context': ( ID_CRED_I, TH_3, CRED_I, ? EAD_3 )
		// The actual 'context' is a CBOR byte string with value the serialization of the CBOR sequence
        List<CBORObject> objectList = new ArrayList<>();
    	objectList.add(idCredI);
    	objectList.add(CBORObject.FromObject(th3));
    	objectList.add(CBORObject.DecodeFromBytes(credI));
    	
    	if (ead3 != null && ead3.length != 0) {
	    	for (int i = 0; i < ead3.length; i++)
	    		objectList.add(ead3[i]);
    	}
    	byte[] contextSequence = Util.buildCBORSequence(objectList);
    	CBORObject context = CBORObject.FromObject(contextSequence);
    	
    	int macLength = 0;
    	int method = session.getMethod();
    	int selectedCipherSuite = session.getSelectedCipherSuite();
    	if (method == 0 || method == 1) {
    		macLength = EdhocSession.getEdhocHashAlgOutputSize(selectedCipherSuite);
    	}
    	if (method == 2 || method == 3) {
    		macLength = EdhocSession.getTagLengthEdhocAEAD(selectedCipherSuite);
    	}
    	
    	byte[] mac3 = null;
    	
    	try {
			mac3 = session.edhocKDF(prk4e3m, Constants.KDF_LABEL_MAC_3, context, macLength);
		} catch (InvalidKeyException e) {
			System.err.println("Error when computing MAC_3\n" + e.getMessage());
		} catch (NoSuchAlgorithmException e) {
			System.err.println("Error when computing MAC_3\n" + e.getMessage());
		}
        if (debugPrint) {
        	Util.nicePrint("context_3", contextSequence);
        }
		
		return mac3;
		
	}
	
	
    /**
     *  Compute CIPHERTEXT_3
     * @param session   The used EDHOC session
     * @param externalData   The External Data for the encryption process
     * @param plaintext   The plaintext to encrypt
     * @param k3ae   The encryption key
     * @param iv3ae   The initialization vector
     * @return  The computed CIPHERTEXT_3
     */
	public static byte[] computeCiphertext3(EdhocSession session, byte[] externalData,
			                                byte[] plaintext, byte[] k3ae, byte[] iv3ae) {
		
    	
    	byte[] ciphertext3 = null;
    	
    	int selectedCipherSuite = session.getSelectedCipherSuite();
    	AlgorithmID alg = EdhocSession.getEdhocAEADAlg(selectedCipherSuite);
    	
    	// Prepare the empty content for the COSE protected header
    	CBORObject emptyMap = CBORObject.NewMap();
    	
    	try {
    		ciphertext3 = Util.encrypt(emptyMap, externalData, plaintext, alg, iv3ae, k3ae);
		} catch (CoseException e) {
			System.err.println("Error when computing CIPHERTEXT_3\n" + e.getMessage());
			return null;
		}
		
		return ciphertext3;
		
	}
	
	
    /**
     *  Compute CIPHERTEXT_4
     * @param session   The used EDHOC session
     * @param externalData   The External Data for the encryption process
     * @param plaintext   The plaintext to encrypt
     * @param k4m   The encryption key
     * @param iv4m   The initialization vector
     * @return  The computed CIPHERTEXT_4
     */
	public static byte[] computeCiphertext4(EdhocSession session, byte[] externalData,
			                         		byte[] plaintext, byte[] k4m, byte[] iv4m) {
		
		
    	byte[] ciphertext4 = null;
    	
    	int selectedCipherSuite = session.getSelectedCipherSuite();
    	AlgorithmID alg = EdhocSession.getEdhocAEADAlg(selectedCipherSuite);
    	
    	// Prepare the empty content for the COSE protected header
    	CBORObject emptyMap = CBORObject.NewMap();
    	
    	try {
			ciphertext4 = Util.encrypt(emptyMap, externalData, plaintext, alg, iv4m, k4m);
		} catch (CoseException e) {
			System.err.println("Error when computing CIPHERTEXT_4\n" + e.getMessage());
			return null;
		}
		
		return ciphertext4;
		
	}
	
	
    /**
     *  Decrypt CIPHERTEXT_3
     * @param session   The used EDHOC session
     * @param externalData   The External Data for the encryption process
     * @param plaintext   The ciphertext to decrypt
     * @param k3ae   The decryption key
     * @param iv3ae   The initialization vector
     * @return  The plaintext recovered from CIPHERTEXT_3
     */
	public static byte[] decryptCiphertext3(EdhocSession session, byte[] externalData,
			                                byte[] ciphertext, byte[] k3ae, byte[] iv3ae) {
		
    	byte[] plaintext = null;
    	
    	int selectedCipherSuite = session.getSelectedCipherSuite();
    	AlgorithmID alg = EdhocSession.getEdhocAEADAlg(selectedCipherSuite);
    	
    	// Prepare the empty content for the COSE protected header
    	CBORObject emptyMap = CBORObject.NewMap();
    	
    	try {
    		plaintext = Util.decrypt(emptyMap, externalData, ciphertext, alg, iv3ae, k3ae);
		} catch (CoseException e) {
			System.err.println("Error when decrypting CIPHERTEXT_3\n" + e.getMessage());
			return null;
		}
		
		return plaintext;
		
	}
	
	
    /**
     *  Decrypt CIPHERTEXT_4
     * @param session   The used EDHOC session
     * @param externalData   The External Data for the encryption process
     * @param plaintext   The ciphertext to decrypt
     * @param k3ae   The decryption key
     * @param iv3ae   The initialization vector
     * @return  The plaintext recovered from CIPHERTEXT_4
     */
	public static byte[] decryptCiphertext4(EdhocSession session, byte[] externalData,
			                                byte[] ciphertext, byte[] k4ae, byte[] iv4ae) {
		
    	byte[] plaintext = null;
    	
    	int selectedCipherSuite = session.getSelectedCipherSuite();
    	AlgorithmID alg = EdhocSession.getEdhocAEADAlg(selectedCipherSuite);
    	
    	// Prepare the empty content for the COSE protected header
    	CBORObject emptyMap = CBORObject.NewMap();
    	
    	try {
    		plaintext = Util.decrypt(emptyMap, externalData, ciphertext, alg, iv4ae, k4ae);
		} catch (CoseException e) {
			System.err.println("Error when decrypting CIPHERTEXT_4\n" + e.getMessage());
			return null;
		}
		
		return plaintext;
		
	}

	
    /**
     *  Compute Signature_or_MAC_2 - Only for the Responder
     * @param session   The used EDHOC session
     * @param mac2   The MAC_2 value
     * @param externalData   The external data for the possible signature process, it can be null
     * @return  The computed Signature_or_MAC_2, or null in case of error
     */
	public static byte[] computeSignatureOrMac2(EdhocSession session, byte[] mac2, byte[] externalData) {
		
		byte[] signatureOrMac2 = null;
    	int authenticationMethod = session.getMethod();
    	
    	if (authenticationMethod == Constants.EDHOC_AUTH_METHOD_1 || authenticationMethod == Constants.EDHOC_AUTH_METHOD_3) {
    		// The responder does not use signatures as authentication method, then Signature_or_MAC_2 is equal to MAC_2
    		signatureOrMac2 = new byte[mac2.length];
    		System.arraycopy(mac2, 0, signatureOrMac2, 0, mac2.length);
    	}
    	else if (authenticationMethod == Constants.EDHOC_AUTH_METHOD_0 || authenticationMethod == Constants.EDHOC_AUTH_METHOD_2) {
    		// The responder uses signatures as authentication method, then Signature_or_MAC_2 has to be computed
    		try {
    			OneKey identityKey = session.getKeyPair();
    			int selectedCipherSuite = session.getSelectedCipherSuite();
    			
    			// Consistency check of key type and curve against the selected cipher suite
    			if (Util.checkSignatureKeyAgainstCipherSuite(identityKey, selectedCipherSuite) == false) {
    				System.err.println("Error when signing MAC_2 to produce Signature_or_MAC_2\n");
    				return null;
    			}

    			if (debugPrint) {
		    		Util.nicePrint("External Data for signing MAC_2 to produce Signature_or_MAC_2", externalData);
		    	}
    			
				signatureOrMac2 = Util.computeSignature(session.getIdCred(), externalData, mac2, identityKey);
				
			} catch (CoseException e) {
				System.err.println("Error when signing MAC_2 to produce Signature_or_MAC_2\n" + e.getMessage());
				return null;
			}
    	}
		
    	return signatureOrMac2;
    	
	}
	
	
    /**
     *  Compute Signature_or_MAC_3 - Only for the Initiator
     * @param session   The used EDHOC session
     * @param mac3   The MAC_3 value
     * @param externalData   The external data for the possible signature process, it can be null
     * @return  The computed Signature_or_MAC_3, or null in case of error
     */
	public static byte[] computeSignatureOrMac3(EdhocSession session, byte[] mac3, byte[] externalData) {
		
		byte[] signatureOrMac3 = null;
    	int authenticationMethod = session.getMethod();
    	
    	if (authenticationMethod == Constants.EDHOC_AUTH_METHOD_2 || authenticationMethod == Constants.EDHOC_AUTH_METHOD_3) {
    		// The initiator does not use signatures as authentication method, then Signature_or_MAC_3 is equal to MAC_3
    		signatureOrMac3 = new byte[mac3.length];
    		System.arraycopy(mac3, 0, signatureOrMac3, 0, mac3.length);
    	}
    	else if (authenticationMethod == Constants.EDHOC_AUTH_METHOD_0 || authenticationMethod == Constants.EDHOC_AUTH_METHOD_1) {
    		// The initiator uses signatures as authentication method, then Signature_or_MAC_3 has to be computed
    		try {
    			OneKey identityKey = session.getKeyPair();
    			int selectedCipherSuite = session.getSelectedCipherSuite();
    			
    			// Consistency check of key type and curve against the selected cipher suite
    			if (Util.checkSignatureKeyAgainstCipherSuite(identityKey, selectedCipherSuite) == false) {
    				System.err.println("Error when signing MAC_3 to produce Signature_or_MAC_3\n");
    				return null;
    			}
    			
    			if (debugPrint) {
    			    Util.nicePrint("External Data for signing MAC_3 to produce Signature_or_MAC_3", externalData);
    			}
    			
				signatureOrMac3 = Util.computeSignature(session.getIdCred(), externalData, mac3, identityKey);
				
			} catch (CoseException e) {
				System.err.println("Error when signing MAC_3 to produce Signature_or_MAC_3\n" + e.getMessage());
				return null;
			}
    	}
		
    	return signatureOrMac3;
    	
	}
	
	
    /**
     *  Verify Signature_or_MAC_2, when this contains an actual signature - Only for the Initiator
     * @param session   The used EDHOC session
     * @param signatureOrMac2   The signature value specified as Signature_or_MAC_2
     * @param externalData   The external data for the possible signature process, it can be null
     * @param mac2   The MAC_2 whose signature has to be verified
     * @return  True in case of successful verification, false otherwise
     */
	public static boolean verifySignatureOrMac2(EdhocSession session, byte[] signatureOrMac2, byte[] externalData, byte[] mac2) {
		
		int authenticationMethod = session.getMethod();
		
    	if (authenticationMethod == Constants.EDHOC_AUTH_METHOD_1 || authenticationMethod == Constants.EDHOC_AUTH_METHOD_3) {
    		// The responder does not use signatures as authentication method, then Signature_or_MAC_2 has to be equal to MAC_2
    		return Arrays.equals(signatureOrMac2, mac2);
    	}
    	else if (authenticationMethod == Constants.EDHOC_AUTH_METHOD_0 || authenticationMethod == Constants.EDHOC_AUTH_METHOD_2) {
    		// The responder uses signatures as authentication method, then Signature_or_MAC_2 is a signature to verify
    		
    		OneKey peerIdentityKey = session.getPeerLongTermPublicKey();
			int selectedCipherSuite = session.getSelectedCipherSuite();
			
			// Consistency check of key type and curve against the selected cipher suite
			if (Util.checkSignatureKeyAgainstCipherSuite(peerIdentityKey, selectedCipherSuite) == false) {
				System.err.println("Error when verifying the signature of Signature_or_MAC_2\n");
				return false;
			}
    		
			try {
				return Util.verifySignature(signatureOrMac2, session.getPeerIdCred(),
						                    externalData, mac2, peerIdentityKey);
			} catch (CoseException e) {
				System.err.println("Error when verifying the signature of Signature_or_MAC_2\n" + e.getMessage());
				return false;
			}
		}
		
		return false;
		
	}
	
	
    /**
     *  Verify Signature_or_MAC_3, when this contains an actual signature - Only for the Responder
     * @param session   The used EDHOC session
     * @param signatureOrMac3   The signature value specified as Signature_or_MAC_3
     * @param externalData   The external data for the possible signature process, it can be null
     * @param mac3   The MAC_3 whose signature has to be verified
     * @return  True in case of successful verification, false otherwise
     */
	public static boolean verifySignatureOrMac3(EdhocSession session, byte[] signatureOrMac3, byte[] externalData, byte[] mac3) {
		
		int authenticationMethod = session.getMethod();
		
    	if (authenticationMethod == Constants.EDHOC_AUTH_METHOD_2 || authenticationMethod == Constants.EDHOC_AUTH_METHOD_3) {
    		// The initiator does not use signatures as authentication method, then Signature_or_MAC_3 has to be equal to MAC_3
    		return Arrays.equals(signatureOrMac3, mac3);
    	}
    	else if (authenticationMethod == Constants.EDHOC_AUTH_METHOD_0 || authenticationMethod == Constants.EDHOC_AUTH_METHOD_1) {
    		// The initiator uses signatures as authentication method, then Signature_or_MAC_3 is a signature to verify

    		OneKey peerIdentityKey = session.getPeerLongTermPublicKey();
			int selectedCipherSuite = session.getSelectedCipherSuite();
			
			// Consistency check of key type and curve against the selected cipher suite
			if (Util.checkSignatureKeyAgainstCipherSuite(peerIdentityKey, selectedCipherSuite) == false) {
				System.err.println("Error when verifying the signature of Signature_or_MAC_3\n");
				return false;
			}
    		
			try {
				return Util.verifySignature(signatureOrMac3, session.getPeerIdCred(),
						                    externalData, mac3, peerIdentityKey);
			} catch (CoseException e) {
				System.err.println("Error when verifying the signature of Signature_or_MAC_3\n" + e.getMessage());
				return false;
			}
		}
		
		return false;
		
	}

    /**
     *  Compute the transcript hash TH2
     * @param session   The used EDHOC session
     * @param gY   The G_Y ephemeral key from the EDHOC Message 2, as a serialized CBOR byte string
     * @param hashMessage1   The hash of EDHOC Message 1, as a serialized CBOR byte string
     * @return  The computed TH2
     */
	public static byte[] computeTH2(EdhocSession session, byte[] gY, byte[] hashMessage1) {
	
        byte[] th2 = null;
        
        int selectedCipherSuite = session.getSelectedCipherSuite();
        String hashAlgorithm = EdhocSession.getEdhocHashAlg(selectedCipherSuite);

        byte[] hashInput = new byte[gY.length + hashMessage1.length];
        System.arraycopy(gY, 0, hashInput, 0, gY.length);
        System.arraycopy(hashMessage1, 0, hashInput, gY.length, hashMessage1.length);
		Util.nicePrint("Input to calculate TH_2", hashInput);
        try {
			th2 = Util.computeHash(hashInput, hashAlgorithm);
		} catch (NoSuchAlgorithmException e) {
			System.err.println("Invalid hash algorithm when computing TH2\n" + e.getMessage());
			return null;
			
		}
		
		return th2;
		
	}

    /**
     *  Compute the transcript hash TH3
     * @param session   The used EDHOC session
     * @param th2   The transcript hash TH2, as a serialized CBOR byte string
     * @param plaintext2   The PLAINTEXT_2 from EDHOC Message 2, as a serialized CBOR sequence
     * @param credR   CRED_R as the serialization of a CBOR object
     * @return  The computed TH3
     */
	public static byte[] computeTH3(EdhocSession session, byte[] th2, byte[] plaintext2, byte[] credR) {
	
        byte[] th3 = null;
        int inputLength = th2.length + plaintext2.length + credR.length;
        
        int selectedCipherSuite = session.getSelectedCipherSuite();
        String hashAlgorithm = EdhocSession.getEdhocHashAlg(selectedCipherSuite);
        
        int offset = 0;
        byte[] hashInput = new byte[inputLength];
        System.arraycopy(th2, 0, hashInput, offset, th2.length);
        offset += th2.length;
        System.arraycopy(plaintext2, 0, hashInput, offset, plaintext2.length);
        
        offset += plaintext2.length;
        System.arraycopy(credR, 0, hashInput, offset, credR.length);
        
		Util.nicePrint("Input to calculate TH_3", hashInput);
        try {
			th3 = Util.computeHash(hashInput, hashAlgorithm);
		} catch (NoSuchAlgorithmException e) {
			System.err.println("Invalid hash algorithm when computing TH3\n" + e.getMessage());
			return null;
			
		}
		
		return th3;
		
	}

    /**
     *  Compute the transcript hash TH4
     * @param session   The used EDHOC session
     * @param th3   The transcript hash TH3, as a serialized CBOR byte string
     * @param plaintext3   The PLAINTEXT_3 from EDHOC Message 3, as a serialized CBOR sequence
     * @param credI   CRED_I as the serialization of a CBOR object
     * @return  The computed TH4
     */
	public static byte[] computeTH4(EdhocSession session, byte[] th3, byte[] plaintext3, byte[] credI) {
	
        byte[] th4 = null;
        int inputLength = th3.length + plaintext3.length + credI.length;
        
        int selectedCipherSuite = session.getSelectedCipherSuite();
        String hashAlgorithm = EdhocSession.getEdhocHashAlg(selectedCipherSuite);
        
        int offset = 0;
        byte[] hashInput = new byte[inputLength];
        System.arraycopy(th3, 0, hashInput, offset, th3.length);
        offset += th3.length;
        System.arraycopy(plaintext3, 0, hashInput, offset, plaintext3.length);
        
        offset += plaintext3.length;
        System.arraycopy(credI, 0, hashInput, offset, credI.length);
        
		Util.nicePrint("Input to calculate TH_4", hashInput);
        try {
        	th4 = Util.computeHash(hashInput, hashAlgorithm);
		} catch (NoSuchAlgorithmException e) {
			System.err.println("Invalid hash algorithm when computing TH4\n" + e.getMessage());
			return null;
			
		}
		
		return th4;
		
	}
	
    /**
     *  Encode an EDHOC connection identifier to the CBOR Object to include in an EDHOC message
     * @param identifier   The EDHOC connection identifier
     * @return  The CBOR object encoding the EDHOC connection identifier, or null in case of error
     */
	public static CBORObject encodeIdentifier(byte[] identifier) {
	
		CBORObject identifierCBOR = null;
		
		if (identifier != null && identifier.length != 1) {
			// Encode the EDHOC connection identifier as a CBOR byte string
			identifierCBOR = CBORObject.FromObject(identifier);
		}

		if (identifier != null && identifier.length == 1) {
			int byteValue = Util.bytesToInt(identifier);
					
			if ((byteValue >= 0 && byteValue <= 23) || (byteValue >= 32 && byteValue <= 55)) {
				// The EDHOC connection identifier is in the range 0x00-0x17 or in the range 0x20-0x37.
				// That is, it happens to be the serialization of a CBOR integer with numeric value -24..23
				
				// Encode the EDHOC connection identifier as a CBOR integer
				identifierCBOR = CBORObject.DecodeFromBytes(identifier);
			}
			else {
				// Encode the EDHOC connection identifier as a CBOR byte string
				identifierCBOR = CBORObject.FromObject(identifier);
			}
		}
		
		return identifierCBOR;
		
	}

    /**
     *  Decode an EDHOC connection identifier from the CBOR Object included in an EDHOC message
     * @param identifier   The CBOR object encoding the EDHOC connection identifier
     * @return  The EDHOC connection identifier, or null in case of error
     */
	public static byte[] decodeIdentifier(CBORObject identifierCbor) {
	
		byte[] identifier = null;
			
		if (identifierCbor != null && identifierCbor.getType() == CBORType.ByteString) {
			identifier = identifierCbor.GetByteString();
			
			// Consistency check
			if (identifier.length == 1) {
				int byteValue = Util.bytesToInt(identifier);
				if ((byteValue >= 0 && byteValue <= 23) || (byteValue >= 32 && byteValue <= 55))
					// This EDHOC connection identifier should have been encoded as a CBOR integer
					identifier = null;
			}			
		}
		else if (identifierCbor != null && identifierCbor.getType() == CBORType.Integer) {
			identifier = identifierCbor.EncodeToBytes();
			
			if (identifier.length != 1) {
				// This EDHOC connection identifier is not valid or was not encoded according to deterministic CBOR
				identifier = null;
			}
			
		}

		return identifier;
		
	}
	
    /**
     *  Perform early sanity checks on the EAD field of an incoming EDHOC message.
     *  Remove any EAD item that is either: i) padding; or ii) non-critical and not supported
     * @param objectList   The CBOR sequence possibly including the EAD_X field.
     *                     For EDHOC message_1, the CBOR sequence is the whole EDHOC message_1.
     *                     For EDHOC message_X, with X = (2, 3, 4), the CBOR sequence is PLAINTEXT_X.
     * @param baseIndex   With reference to 'objectList', the index of its CBOR item encoding the ead_label of the first EAD item.
     * @param msnNum      The integer X = (1, 2, 3, 4), consistent with the specifically received EDHOC message_X.
     * @param supportedEADs   The list of EAD items supported by this peer.
     * @return  In case of success, it returns the subset of the EAD field to be passed to the application for further processing.
     *          In case of error, it returns an array including one element as a CBOR text string, whose value provides a description
     *          of the error to be used in the EDHOC error message to return.
     */
	static CBORObject[] preParseEAD(CBORObject[] objectList, int baseIndex, int msgNum, Set<Integer> supportedEADs) {
		
		int length = objectList.length - baseIndex;
		CBORObject[] aux = new CBORObject[length];
		CBORObject[] ret = null;
		int count = 0;
		
		boolean error = false;
		String errMsg = new String("");
		
		// The actual goal of each step is to go through one *EAD item*.
		// At each step, the element with index 'i' must be an ead_label.
		// For EAD items that are supported or non-critical, the corresponding ead_value (if present) is
		// handled during the same step, so that the next step will consider the next EAD item, if any.
		for (int i = baseIndex; i < objectList.length; i++) {
			
			CBORObject myObject = objectList[i];
			
			if (myObject.getType() != CBORType.Integer) {
				 // Each EAD item must start with a CBOR integer encoding the ead_label
				 errMsg = new String("Malformed or invalid EAD_" + msgNum);
		         error = true;
		         break;
			}
        	if (i+1 < objectList.length &&
        		objectList[i+1].getType() != CBORType.Integer &&
        		objectList[i+1].getType() != CBORType.ByteString) {
    			 // The immediately following item in the CBOR sequence (if any) must be a CBOR integer or a CBOR byte string
				 errMsg = new String("Malformed or invalid EAD_" + msgNum);
		         error = true;
		         break;
        	}

            int eadLabel = myObject.AsInt32();
            
            if (eadLabel == Constants.EAD_LABEL_PADDING) {
            	// This is the padding EAD item and it is not passed to the application for further processing
            	
            	if (debugPrint) {
            		System.out.println("ead_label: " + eadLabel);
            	}
            	
            	if (i+1 < objectList.length && objectList[i+1].getType() == CBORType.ByteString) {
            		if (debugPrint) {
            			Util.nicePrint("ead_value", objectList[i+1].GetByteString());
            		}
            		
	            	// Skip the corresponding ead_value, if present
            		i++;
            	}
            	continue;
            }
            
            int eadLabelUnsigned = (eadLabel < 0) ? (-eadLabel) : eadLabel;
            boolean supported = supportedEADs.contains(Integer.valueOf(eadLabelUnsigned));

            if (!supported) {
	            if (eadLabel < 0) {
	            	// Since the EAD item is critical and is not supported, the protocol must be discontinued
	                errMsg = new String("Unsupported EAD_" + msgNum + " critical item with ead_label " + eadLabelUnsigned);
	                error = true;
	                break;
	            }
	            else {
	            	// Since the EAD item is non-critical and is not supported,
	            	// it is not passed to the application for further processing
	            	if (i+1 < objectList.length && objectList[i+1].getType() == CBORType.ByteString) {
	                    i++; // This will result in moving to the next EAD item, if any
	            	}
                    continue;
	            }
            }

            // This EAD item is supported, so it is kept for further processing

            // Make a hard copy of the ead_label
            byte[] serializedObjectLabel = myObject.EncodeToBytes();
            CBORObject elementLabel = CBORObject.DecodeFromBytes(serializedObjectLabel);
            aux[count] = elementLabel;
            count++;
            
            if (debugPrint) {
            	System.out.println("ead_label: " + eadLabel);
            }
            
            // Make a hard copy of the ead_value, if present
        	if (i+1 < objectList.length && objectList[i+1].getType() == CBORType.ByteString) {
                byte[] serializedObjectValue = objectList[i+1].EncodeToBytes();
                CBORObject elementValue = CBORObject.DecodeFromBytes(serializedObjectValue);
                aux[count] = elementValue;
                count++;
                
                if (debugPrint) {
                	Util.nicePrint("ead_value", objectList[i+1].GetByteString());
                }
                
                i++; // This will result in moving to the next EAD item, if any
        	}

		}
		
		if (error == true) {
			// Prepare the diagnostic error text to be returned
			ret = new CBORObject[1];
			ret[0] = CBORObject.FromObject(errMsg);
		}
		else {
			// Prepare the subset of the EAD items to provide to the application for further processing
			ret = new CBORObject[count];
			for (int i = 0; i < count; i++)
				ret[i] = aux[i];
		}
		
		return ret;
		
	}
	
}