Token.java

/*******************************************************************************
 * Copyright (c) 2019, RISE AB
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions 
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, 
 *    this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice, 
 *    this list of conditions and the following disclaimer in the documentation 
 *    and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *******************************************************************************/
package se.sics.ace.as;

import java.nio.ByteBuffer;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;

import org.bouncycastle.crypto.InvalidCipherTextException;
import org.eclipse.californium.elements.auth.RawPublicKeyIdentity;

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

import org.eclipse.californium.cose.CoseException;
import org.eclipse.californium.cose.Encrypt0Message;
import org.eclipse.californium.cose.HeaderKeys;
import org.eclipse.californium.cose.KeyKeys;
import org.eclipse.californium.cose.OneKey;

import se.sics.ace.AccessToken;
import se.sics.ace.AceException;
import se.sics.ace.Constants;
import se.sics.ace.Endpoint;
import se.sics.ace.Message;
import se.sics.ace.TimeProvider;
import se.sics.ace.Util;
import se.sics.ace.as.logging.DhtLogger;
import static se.sics.ace.as.logging.Const.*;
import se.sics.ace.cwt.CWT;
import se.sics.ace.cwt.CwtCryptoCtx;

/**
 * Implements the /token endpoint on the authorization server.
 * 
 * Note: If a client requests a scope that is not supported by (parts) of the audience this endpoint will just ignore
 * that, assuming that the client will be denied by the PDP anyway. This requires a default deny policy in the PDP.
 * 
 * Note: This endpoint assigns a cti to each issued token based on a counter. The same value is also used as kid for the
 * proof-of-possession key associated to the token by means of the 'cnf' claim.
 * 
 * Note: This endpoint assumes that the sender Id (the one you get from Message.getSenderId()) for a secure session
 * created with a raw public key is generated with org.eclipse.californium.scandium.auth.RawPublicKeyIdentity.getName()
 * 
 * @author Ludwig Seitz and Marco Tiloca
 *
 */
public class Token implements Endpoint, AutoCloseable {

    /**
     * The logger
     */
    private static final Logger LOGGER = Logger.getLogger(Token.class.getName());

    /**
     * Boolean for not verify
     */
    private static boolean sign = false;

    /**
     * The PDP this endpoint uses to make access control decisions.
     */
    private PDP pdp;

    /**
     * The database connector for storing and retrieving stuff.
     */
    private DBConnector db;

    /**
     * The identifier of this AS for the iss claim.
     */
    private String asId;

    /**
     * The time provider for this AS.
     */
    private TimeProvider time;

    /**
     * The default expiration time of an access token
     */
    private static long expiration = 1000 * 60 * 10; // 10 minutes

    /**
     * The counter for generating the cti
     */
    private Long cti = 0L;

    /**
     * The private key of the AS or null if there isn't any
     */
    private OneKey privateKey;

    /**
     * The client credentials grant type as CBOR-integer
     */
    public static CBORObject clientCredentials = CBORObject.FromObject(Constants.GT_CLI_CRED);

    /**
     * The authorizaton_code grant type as CBOR-integer
     */
    public static CBORObject authzCode = CBORObject.FromObject(Constants.GT_AUTHZ_CODE);

    /**
     * Converter to create the byte array from the cti number
     */
    private static ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);

    /**
     * The claim types included in tokens generated by this Token instance
     */
    private Set<Short> claims;

    private static Set<Short> defaultClaims = new HashSet<>();

    static {
        defaultClaims.add(Constants.CTI);
        defaultClaims.add(Constants.ISS);
        defaultClaims.add(Constants.EXI);
        defaultClaims.add(Constants.AUD);
        defaultClaims.add(Constants.SCOPE);
        defaultClaims.add(Constants.CNF);
    }

    /**
     * If true the AUD claim is inserted in the COSE header of a CWT generated by this AS in order to be able to
     * retrieve the right keys when the CWT is presented by the client instead of the RS for introspection
     */
    private boolean setAudHeader = false;

    /**
     * Incremented after having released an Access Token including OSCORE input material The current value is used for
     * the 'id' parameter in the OSCORE Security Context object in 'cnf'
     */
    private int OSCORE_material_counter = 0;

    /**
     * Store the association between the cti of an issued Access Token and the target audience intended to consume it.
     */
    private Map<String, String> cti2aud = new HashMap<>();

    /**
     * Store the association between the name of the Resource Server and the next value to use as Sequence Number to
     * build the 'cti' claim when the 'exi' claim is included in the Access Token
     * 
     * The entry for a Resource Server is created when the first Access Token including 'exi' is issues, since the AS
     * process has started. The initial value of the Sequence Number is retrieved from the database.
     */
    private Map<String, Integer> exiSequenceNumbers = new HashMap<>();

    /**
     * Relevant only when the DTLS profile is used with symmetric PoP key
     * 
     * Store the association between the cti of an issued Acced Token and the 'kid' of the associated symmetric PoP key
     * generated by the AS
     */
    private Map<String, CBORObject> cti2kid = new HashMap<>();

    /**
     * Relevant only when the OSCORE profile is used
     * 
     * Store the association between the cti of an issued Acced Token and the ID identifying the OSCORE Input Material.
     * Such an ID is stored as a CBOR byte string.
     */
    private Map<String, CBORObject> cti2oscId = new HashMap<>();

    /**
     * Relevant only when the OSCORE profile is used
     * 
     * The size in bytes of the OSCORE Master Salt to provide to the Client and to include in the Token. It can be 0, to
     * not provide a Master Salt.
     */
    private short masterSaltSize;

    /**
     * Relevant only when the OSCORE profile is used
     * 
     * True if the OSCORE Id Context has to be provided, false otherwise
     */
    private boolean provideIdContext;

    /**
     * Relevant only when the OSCORE profile is used
     * 
     * It specifies information on the next Id Context to assign for each Resource Server
     */
    private Map<String, IdContextInfo> idContextInfoMap = new HashMap<>();

    /**
     * Mapping between security identities of the peers and their names; it can be null
     * 
     * This is relevant especially for the OSCORE profile, since all peers are registered in the AS database by
     * nicknames. Instead, their OSCORE identities as retrieved from incoming OSCORE messages are structured base64
     * strings encoding the Context ID and Sender ID for that peer
     */
    private Map<String, String> peerIdentitiesToNames = null;

    /**
     * Constructor using default set of claims.
     * 
     * @param asId the identifier of this AS
     * @param pdp the PDP for deciding access
     * @param db the database connector
     * @param time the time provider
     * @param privateKey the private key of the AS or null if there isn't any
     * @param peerIdentitiesToNames mapping between security identities of the peers and their names; it can be null
     * 
     * @throws AceException if fetching the cti from the database fails
     */
    public Token(String asId, PDP pdp, DBConnector db, TimeProvider time, OneKey privateKey,
            Map<String, String> peerIdentitiesToNames) throws AceException {
        this(asId, pdp, db, time, privateKey, defaultClaims, false, (short) 0, false, peerIdentitiesToNames);
    }

    /**
     * Constructor that allows configuration of the claims included in the token.
     * 
     * @param asId the identifier of this AS
     * @param pdp the PDP for deciding access
     * @param db the database connector
     * @param time the time provider
     * @param privateKey the private key of the AS or null if there isn't any
     * @param claims the claim types to include in tokens issued by this Token instance
     * @param setAudInCwtHeader if true the AUD claim is inserted in the COSE header of a CWT generated by this AS in
     *        order to be able to retrieve the right keys when the CWT is presented by the client instead of the RS for
     *        introspection
     * @param peerIdentitiesToNames mapping between security identities of the peers and their names; it can be null
     * 
     * @throws AceException if fetching the cti from the database fails
     */
    public Token(String asId, PDP pdp, DBConnector db, TimeProvider time, OneKey privateKey, Set<Short> claims,
            boolean setAudInCwtHeader, Map<String, String> peerIdentitiesToNames) throws AceException {
        this(asId, pdp, db, time, privateKey, claims, setAudInCwtHeader, (short) 0, false, peerIdentitiesToNames);
    }

    /**
     * Constructor that allows configuration of everything.
     * 
     * @param asId the identifier of this AS
     * @param pdp the PDP for deciding access
     * @param db the database connector
     * @param time the time provider
     * @param privateKey the private key of the AS or null if there isn't any
     * @param claims the claim types to include in tokens issued by this Token instance
     * @param setAudInCwtHeader if true the AUD claim is inserted in the COSE header of a CWT generated by this AS in
     *        order to be able to retrieve the right keys when the CWT is presented by the client instead of the RS for
     *        introspection
     * @param masterSaltSize the size in bytes of the OSCORE Master Salt
     * @param provideIdContext true if the OSCORE Id Context has to be provided, false otherwise
     * @param peerIdentitiesToNames mapping between security identities of the peers and their names; it can be null
     * 
     * @throws AceException if fetching the cti from the database fails
     */
    public Token(String asId, PDP pdp, DBConnector db, TimeProvider time, OneKey privateKey, Set<Short> claims,
            boolean setAudInCwtHeader, short masterSaltSize, boolean provideIdContext,
            Map<String, String> peerIdentitiesToNames) throws AceException {

        Set<Short> localClaims = claims;

        if (localClaims == null) {
            localClaims = defaultClaims;
        }

        // Time for checks
        if (asId == null || asId.isEmpty()) {
            LOGGER.severe("Token endpoint's AS identifier was null or empty");
            throw new AceException("AS identifier must be non-null and non-empty");
        }
        if (pdp == null) {
            LOGGER.severe("Token endpoint's PDP was null");
            throw new AceException("Token endpoint's PDP must be non-null");
        }
        if (db == null) {
            LOGGER.severe("Token endpoint's DBConnector was null");
            throw new AceException("Token endpoint's DBConnector must be non-null");
        }
        if (time == null) {
            LOGGER.severe("Token endpoint's TimeProvider was null");
            throw new AceException("Token endpoint's TimeProvider " + "must be non-null");
        }
        // All checks passed
        this.asId = asId;
        this.pdp = pdp;
        this.db = db;
        this.time = time;
        this.privateKey = privateKey;
        this.cti = db.getCtiCounter();
        this.claims = new HashSet<>();
        this.claims.addAll(localClaims);
        this.setAudHeader = setAudInCwtHeader;
        this.masterSaltSize = masterSaltSize;
        this.provideIdContext = provideIdContext;
        this.peerIdentitiesToNames = peerIdentitiesToNames;

    }

    @Override
    public Message processMessage(Message msg) {
        // Purge expired tokens from the database
        try {
            this.db.purgeExpiredTokens(this.time.getCurrentTime());
        } catch (AceException e) {
            LOGGER.severe("Database error: " + e.getMessage());
			DhtLogger.sendLog(TYPE_ERROR, PRIO_HIGH, CAT_STATUS, DEVICE_NAME, "Database error");
            return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
        }

        if (msg == null) {// This should not happen
            LOGGER.severe("Token.processMessage() received null message");
            return null;
        }
        LOGGER.log(Level.INFO, "Token received message: " + msg.getParameters());

        // 1. Check if this client can request tokens
        String id = msg.getSenderId();
        if (id == null) {
            CBORObject map = CBORObject.NewMap();
            map.Add(Constants.ERROR, Constants.UNAUTHORIZED_CLIENT);
            LOGGER.log(Level.INFO, "Message processing aborted: " + "unauthorized client");
			DhtLogger.sendLog(TYPE_WARNING, PRIO_MEDIUM, CAT_STATUS, DEVICE_NAME,
					"Message processing aborted: " + "unauthorized client: " + id);
            return msg.failReply(Message.FAIL_UNAUTHORIZED, map);
        }

        if (peerIdentitiesToNames != null) {
            id = peerIdentitiesToNames.get(id);
            if (id == null) {
                CBORObject map = CBORObject.NewMap();
                map.Add(Constants.ERROR, Constants.UNAUTHORIZED_CLIENT);
				LOGGER.log(Level.INFO, "Message processing aborted: " + "unauthorized client");
				DhtLogger.sendLog(TYPE_WARNING, PRIO_MEDIUM, CAT_STATUS, DEVICE_NAME,
						"Message processing aborted: " + "unauthorized client: " + id);
                return msg.failReply(Message.FAIL_UNAUTHORIZED, map);
            }
        }

        try {
            if (!this.pdp.canAccessToken(id)) {
                CBORObject map = CBORObject.NewMap();
                map.Add(Constants.ERROR, Constants.UNAUTHORIZED_CLIENT);
                LOGGER.log(Level.INFO, "Message processing aborted: " + "unauthorized client");
				DhtLogger.sendLog(TYPE_WARNING, PRIO_MEDIUM, CAT_STATUS, DEVICE_NAME,
						"Message processing aborted: " + "unauthorized client: " + id);
                return msg.failReply(Message.FAIL_UNAUTHORIZED, map);
            }
        } catch (AceException e) {
            LOGGER.severe("Database error: " + e.getMessage());
			DhtLogger.sendLog(TYPE_ERROR, PRIO_HIGH, CAT_STATUS, DEVICE_NAME, "Database error");
            return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
        }

        // 2. Check that this is a supported grant type
        if (msg.getParameter(Constants.GRANT_TYPE) == null
                // grant type == client credentials implied
                || msg.getParameter(Constants.GRANT_TYPE).equals(clientCredentials)) {
            return processCC(msg);
        } else if (msg.getParameter(Constants.GRANT_TYPE).equals(authzCode)) {
            return processAC(msg);
        }
        CBORObject map = CBORObject.NewMap();
        map.Add(Constants.ERROR, Constants.UNSUPPORTED_GRANT_TYPE);
        LOGGER.log(Level.INFO, "Message processing aborted: " + "unsupported_grant_type");
		DhtLogger.sendLog(TYPE_WARNING, PRIO_MEDIUM, CAT_STATUS, DEVICE_NAME,
				"Message processing aborted: " + "unsupported_grant_type");
        return msg.failReply(Message.FAIL_BAD_REQUEST, map);
    }

    /**
     * Process a Client Credentials grant.
     * 
     * @param msg the message
     * @param id the identifier of the requester
     * 
     * @return the reply
     */
    private Message processCC(Message msg) {
        String id = msg.getSenderId();

        if (peerIdentitiesToNames != null) {
            id = peerIdentitiesToNames.get(id);
            if (id == null) {
                CBORObject map = CBORObject.NewMap();
                map.Add(Constants.ERROR, Constants.UNAUTHORIZED_CLIENT);
                LOGGER.log(Level.INFO, "Message processing aborted: " + "unauthorized client: " + id);
                DhtLogger.sendLog(TYPE_WARNING, PRIO_MEDIUM, CAT_STATUS, DEVICE_NAME,
                        "Message processing aborted: " + "unauthorized client: " + id);
                return msg.failReply(Message.FAIL_UNAUTHORIZED, map);
            }
        }

        // 3. Check if the request has a scope
        CBORObject cbor = msg.getParameter(Constants.SCOPE);
        Object scope = null;
        if (cbor == null) {
            try {
                scope = this.db.getDefaultScope(id);
            } catch (AceException e) {
                LOGGER.severe("Message processing aborted (checking scope): " + e.getMessage());
                DhtLogger.sendLog(TYPE_ERROR, PRIO_HIGH, CAT_STATUS, DEVICE_NAME,
                        "Message processing aborted (checking scope)");
                return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
            }
        } else {
            if (cbor.getType().equals(CBORType.TextString)) {
                scope = cbor.AsString();
            } else if (cbor.getType().equals(CBORType.ByteString)) {
                scope = cbor.GetByteString();
            } else {
                CBORObject map = CBORObject.NewMap();
                map.Add(Constants.ERROR, Constants.INVALID_REQUEST);
                map.Add(Constants.ERROR_DESCRIPTION, "Invalid datatype for scope");
                LOGGER.log(Level.INFO, "Message processing aborted: " + "Invalid datatype for scope in message");
                DhtLogger.sendLog(TYPE_WARNING, PRIO_MEDIUM, CAT_STATUS, DEVICE_NAME,
                        "Message processing aborted: " + "Invalid datatype for scope in message");
                return msg.failReply(Message.FAIL_BAD_REQUEST, map);
            }
        }
        if (scope == null) {
            CBORObject map = CBORObject.NewMap();
            map.Add(Constants.ERROR, Constants.INVALID_REQUEST);
            map.Add(Constants.ERROR_DESCRIPTION, "No scope found for message");
            LOGGER.log(Level.INFO, "Message processing aborted: " + "No scope found for message");
            DhtLogger.sendLog(TYPE_WARNING, PRIO_MEDIUM, CAT_STATUS, DEVICE_NAME,
                    "Message processing aborted: " + "No scope found for message");
            return msg.failReply(Message.FAIL_BAD_REQUEST, map);
        }

        // 4. Check if the request has an audience or if there is a default
        // audience
        cbor = msg.getParameter(Constants.AUDIENCE);

        // The audience has to be a text string. A set is built for
        // compatibility with other methods
        Set<String> aud = new HashSet<>();

        String audStr = ""; // used to save the audience for later, for possible
                            // update of access rights
        String oldCti = ""; // used to track the cti of a Token to supersede, in
                            // case of update of access rights

        if (cbor == null) {
            try {
                String dAud = this.db.getDefaultAudience(id);
                if (dAud != null) {
                    aud.add(dAud);
                    audStr = new String(dAud);
                }
            } catch (AceException e) {
                LOGGER.severe("Message processing aborted (checking aud): " + e.getMessage());
                DhtLogger.sendLog(TYPE_ERROR, PRIO_HIGH, CAT_STATUS, DEVICE_NAME,
                        "Message processing aborted (checking aud)");
                return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
            }
        } else {
            if (cbor.getType().equals(CBORType.TextString)) {
                aud.add(cbor.AsString());
                audStr = new String(cbor.AsString());
            } else {// error
                CBORObject map = CBORObject.NewMap();
                map.Add(Constants.ERROR, Constants.INVALID_REQUEST);
                map.Add(Constants.ERROR_DESCRIPTION, "Audience malformed");
                LOGGER.log(Level.INFO, "Message processing aborted: " + "Audience malformed");
                DhtLogger.sendLog(TYPE_WARNING, PRIO_MEDIUM, CAT_STATUS, DEVICE_NAME,
                        "Message processing aborted: " + "Audience malformed");
                return msg.failReply(Message.FAIL_BAD_REQUEST, map);
            }
        }
        if (aud.isEmpty()) {
            CBORObject map = CBORObject.NewMap();
            map.Add(Constants.ERROR, Constants.INVALID_REQUEST);
            map.Add(Constants.ERROR_DESCRIPTION, "No audience found for message");
            LOGGER.log(Level.INFO, "Message processing aborted: " + "No audience found for message");
            DhtLogger.sendLog(TYPE_WARNING, PRIO_MEDIUM, CAT_STATUS, DEVICE_NAME,
                    "Message processing aborted: " + "No audience found for message");
            return msg.failReply(Message.FAIL_BAD_REQUEST, map);
        }

        // 5. Check if the scope is allowed
        Object allowedScopes = null;
        try {
            allowedScopes = this.pdp.canAccess(id, aud, scope);
        } catch (AceException e) {
            LOGGER.severe("Message processing aborted (checking permissions): " + e.getMessage());
            DhtLogger.sendLog(TYPE_ERROR, PRIO_HIGH, CAT_STATUS, DEVICE_NAME,
                    "Message processing aborted (checking permissions)");
            return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
        }
        if (allowedScopes == null) {
            CBORObject map = CBORObject.NewMap();
            map.Add(Constants.ERROR, Constants.INVALID_SCOPE);
            LOGGER.log(Level.INFO, "Message processing aborted: " + "invalid_scope");
            DhtLogger.sendLog(TYPE_WARNING, PRIO_MEDIUM, CAT_STATUS, DEVICE_NAME,
                    "Message processing aborted: " + "invalid_scope");
            return msg.failReply(Message.FAIL_BAD_REQUEST, map);
        }

        // 6. Create token
        // Find supported token type
        Short tokenType = null;
        try {
            tokenType = this.db.getSupportedTokenType(aud);
        } catch (AceException e) {
            LOGGER.severe("Message processing aborted (creating token): " + e.getMessage());
            DhtLogger.sendLog(TYPE_ERROR, PRIO_HIGH, CAT_STATUS, DEVICE_NAME,
                    "Message processing aborted (creating token)");
            return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
        }
        if (tokenType == null) {
            CBORObject map = CBORObject.NewMap();
            map.Add(Constants.ERROR, "Audience incompatible on token type");
            LOGGER.log(Level.INFO, "Message processing aborted: " + "Audience incompatible on token type");
            DhtLogger.sendLog(TYPE_WARNING, PRIO_MEDIUM, CAT_STATUS, DEVICE_NAME,
                    "Message processing aborted: " + "Audience incompatible on token type");
            return msg.failReply(Message.FAIL_BAD_REQUEST, map);
        }

        boolean includeExi = this.claims.contains(Constants.EXI);
        // If the 'exi' claim is included, ensure that the 'cti' claim is also
        // included
        if (includeExi) {
            this.claims.add(Constants.CTI);
        }

        // The construction of 'cti' depends on the presence/absence of the
        // 'exi' claim.
        //
        // If the 'exi' claim is not present, 'cti' is the serialization of a
        // global counter.
        //
        // If the 'exi' claim is present, 'cti' is the serialization of two
        // concatenated strings, i.e., the name of the Resource Server and the
        // current value of the Exi Sequence Number
        byte[] ctiB = null;
        String ctiStr = null;
        String rsName = null;
        int exiSeqNum = -1;
        if (!includeExi) {
            // The 'exi' claim is not included in the Access Token.
            // Thus, 'cti' can be easily built by using the related single
            // counter
            ctiB = buffer.putLong(0, this.cti).array();
            ctiStr = Base64.getEncoder().encodeToString(ctiB);
            this.cti++;
        } else {
            // The 'exi' claim is included in the Access Token.
            //
            // Thus, 'cti' has to be built according to a particular semantics,
            // as the serialization of the text string S1 = (S2 | S3), where S2
            // is the name of the Resource Server and S3 is the text encoding of
            // the Exi Sequence Number to use for that Resource Server.

            // Determine the name of the Resource Server associated to the
            // specified Audience
            Set<String> rsSet = new HashSet<>();
            try {
                rsSet = db.getRSS(audStr);
            } catch (AceException e) {
                LOGGER.severe("Message processing aborted: Error when retrieving the name"
                        + " of the Resource Server with Audience " + audStr + " from the database.\n" + e.getMessage());
                DhtLogger.sendLog(TYPE_ERROR, PRIO_HIGH, CAT_STATUS, DEVICE_NAME,
                        "Message processing aborted: Error when retrieving the name"
                                + " of the Resource Server with Audience " + audStr + " from the database.");
                return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
            }
            // Check the the specified Audience is associated to exactly one
            // Resource Server
            if (rsSet.size() != 1) {
                CBORObject map = CBORObject.NewMap();
                map.Add(Constants.ERROR, Constants.INVALID_REQUEST);
                map.Add(Constants.ERROR_DESCRIPTION, "The 'exi' claim has to be included, thus Audience must contain"
                        + " exactly one Resource Server");
                LOGGER.log(Level.INFO, "Message processing aborted: The 'exi' claim has to be included,"
                        + "thus Audience must contain exactly one Resource Server");
                DhtLogger.sendLog(TYPE_WARNING, PRIO_MEDIUM, CAT_STATUS, DEVICE_NAME,
                        "Message processing aborted: The 'exi' claim has to be included,"
                                + "thus Audience must contain exactly one Resource Server");
                return msg.failReply(Message.FAIL_BAD_REQUEST, map);
            }
            for (String rs : rsSet)
                rsName = new String(rs);

            // Retrieve the value of the Exi Sequence Number to use for this
            // Resource Server
            if (exiSequenceNumbers.containsKey(rsName)) {
                exiSeqNum = exiSequenceNumbers.get(rsName).intValue();
            } else {
                // This is going to be the first Access Token including the
                // 'exi' claim issued to this Resource Server since the AS
                // process started. Then, retrieve the current Exi Sequence
                // Number value for this Resource Server from the database.
                try {
                    exiSeqNum = db.getExiSequenceNumber(rsName);
                } catch (AceException e) {
                    LOGGER.severe("Message processing aborted: Error when retrieving the Exi Sequence Number"
                            + " for the Resource Server with Audience " + audStr + " from the database.\n"
                            + e.getMessage());
                    DhtLogger.sendLog(TYPE_ERROR, PRIO_HIGH, CAT_STATUS, DEVICE_NAME,
                            "Message processing aborted: Error when retrieving the Exi Sequence Number"
                                    + " for the Resource Server with Audience " + audStr + " from the database.");
                    return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
                }
            }

            // Update the local collection of Exi Sequence Numbers
            Integer newSeqNum = Integer.valueOf(exiSeqNum + 1);
            exiSequenceNumbers.put(rsName, newSeqNum);

            String rawCti = new String(rsName + String.valueOf(exiSeqNum));
            ctiB = rawCti.getBytes(Constants.charset);
            ctiStr = Base64.getEncoder().encodeToString(ctiB);

        }

        // Find supported profile

        String profileStr = null;
        try {
            profileStr = this.db.getSupportedProfile(id, aud);
        } catch (AceException e) {
            if (!includeExi) {
                this.cti--; // roll-back
            } else {
                // roll-back
                exiSequenceNumbers.put(rsName, exiSeqNum);
            }
            LOGGER.severe("Message processing aborted (finding profile): " + e.getMessage());
            DhtLogger.sendLog(TYPE_ERROR, PRIO_HIGH, CAT_STATUS, DEVICE_NAME,
                    "Message processing aborted (finding profile)");
            return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
        }
        if (profileStr == null) {
            if (!includeExi) {
                this.cti--; // roll-back
            } else {
                // roll-back
                exiSequenceNumbers.put(rsName, exiSeqNum);
            }
            CBORObject map = CBORObject.NewMap();
            map.Add(Constants.ERROR, Constants.INCOMPATIBLE_PROFILES);
            LOGGER.log(Level.INFO, "Message processing aborted: " + "No compatible profile found");
            DhtLogger.sendLog(TYPE_WARNING, PRIO_MEDIUM, CAT_STATUS, DEVICE_NAME,
                    "Message processing aborted: " + "No compatible profile found");
            return msg.failReply(Message.FAIL_BAD_REQUEST, map);
        }
        short profile = Constants.getProfileAbbrev(profileStr);

        if (tokenType != AccessTokenFactory.CWT_TYPE && tokenType != AccessTokenFactory.REF_TYPE) {
            if (!includeExi) {
                this.cti--; // roll-back
            } else {
                // roll-back
                exiSequenceNumbers.put(rsName, exiSeqNum);
            }
            CBORObject map = CBORObject.NewMap();
            map.Add(Constants.ERROR, "Unsupported token type");
            LOGGER.log(Level.INFO, "Message processing aborted: " + "Unsupported token type");
            DhtLogger.sendLog(TYPE_WARNING, PRIO_MEDIUM, CAT_STATUS, DEVICE_NAME,
                    "Message processing aborted: " + "Unsupported token type");
            return msg.failReply(Message.FAIL_NOT_IMPLEMENTED, map);
        }

        // This flag will be set to true if the Token is intended to update
        // access rights
        boolean updateAccessRights = false;

        String keyType = null; // Save the key type for later
        Map<Short, CBORObject> claims = new HashMap<>();

        // ISS SUB AUD EXP NBF IAT CTI SCOPE CNF RS_CNF PROFILE EXI
        for (Short c : this.claims) {
            switch (c) {
            case Constants.ISS:
                claims.put(Constants.ISS, CBORObject.FromObject(this.asId));
                break;
            case Constants.SUB:
                claims.put(Constants.SUB, CBORObject.FromObject(id));
                break;
            case Constants.AUD:
                // Check if AUDIENCE is a singleton
                if (aud.size() == 1) {
                    claims.put(Constants.AUD, CBORObject.FromObject(aud.iterator().next()));
                } else {
                    claims.put(Constants.AUD, CBORObject.FromObject(aud));
                }
                break;
            case Constants.EXP:
                long now = this.time.getCurrentTime();
                long exp = Long.MAX_VALUE;
                try {
                    exp = this.db.getExpTime(aud);
                } catch (AceException e) {
                    LOGGER.severe("Message processing aborted (setting exp): " + e.getMessage());
                    DhtLogger.sendLog(TYPE_ERROR, PRIO_HIGH, CAT_STATUS, DEVICE_NAME,
                            "Message processing aborted (setting exp)");
                    return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
                }
                if (exp == Long.MAX_VALUE) { // == No expiration time found
                    // using default
                    exp = now + expiration;
                } else {
                    exp = now + exp;
                }
                claims.put(Constants.EXP, CBORObject.FromObject(exp));
                break;
            case Constants.EXI:
                long exi = Long.MAX_VALUE;
                try {
                    exi = this.db.getExpTime(aud);
                } catch (AceException e) {
                    LOGGER.severe("Message processing aborted (setting exp): " + e.getMessage());
                    DhtLogger.sendLog(TYPE_ERROR, PRIO_HIGH, CAT_STATUS, DEVICE_NAME,
                            "Message processing aborted (setting exp)");
                    return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
                }
                if (exi == Long.MAX_VALUE) { // == No expiration time found
                    // using default
                    exi = expiration;
                }
                claims.put(Constants.EXI, CBORObject.FromObject(exi));
                break;
            case Constants.NBF:
                // XXX: NBF is not configurable in this version
                now = this.time.getCurrentTime();
                claims.put(Constants.NBF, CBORObject.FromObject(now));
                break;
            case Constants.IAT:
                now = this.time.getCurrentTime();
                claims.put(Constants.IAT, CBORObject.FromObject(now));
                break;
            case Constants.CTI:
                claims.put(Constants.CTI, CBORObject.FromObject(ctiB));
                break;
            case Constants.SCOPE:
                claims.put(Constants.SCOPE, CBORObject.FromObject(allowedScopes));
                break;
            case Constants.CNF:
                CBORObject cnf = msg.getParameter(Constants.REQ_CNF);
                if (cnf == null) { // The client wants to use PSK
                    keyType = "PSK"; // save for later

                    // check if PSK is supported for proof-of-possession
                    try {
                        if (!isSupported(keyType, aud)) {
                            if (!includeExi) {
                                this.cti--; // roll-back
                            } else {
                                // roll-back
                                exiSequenceNumbers.put(rsName, exiSeqNum);
                            }
                            CBORObject map = CBORObject.NewMap();
                            map.Add(Constants.ERROR, Constants.UNSUPPORTED_POP_KEY);
                            LOGGER.log(Level.INFO, "Message processing aborted: " + "Unsupported pop key type PSK");
                            DhtLogger.sendLog(TYPE_WARNING, PRIO_MEDIUM, CAT_STATUS, DEVICE_NAME,
                                    "Message processing aborted: " + "Unsupported pop key type PSK");
                            return msg.failReply(Message.FAIL_BAD_REQUEST, map);
                        }
                    } catch (AceException e) {
                        if (!includeExi) {
                            this.cti--; // roll-back
                        } else {
                            // roll-back
                            exiSequenceNumbers.put(rsName, exiSeqNum);
                        }
                        LOGGER.severe("Message processing aborted " + "(finding key type): " + e.getMessage());
                        DhtLogger.sendLog(TYPE_ERROR, PRIO_HIGH, CAT_STATUS, DEVICE_NAME,
                                "Message processing aborted " + "(finding key type)");
                        return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
                    }

                    // Audience supports PSK, make a new PSK
                    try {
                        KeyGenerator kg = KeyGenerator.getInstance("AES");

                        // OSCORE profile
                        if (profile == Constants.COAP_OSCORE) {
                            // Generate OSCORE cnf
                            SecretKey key = kg.generateKey();
                            byte[] masterSecret = key.getEncoded();
                            CBORObject osc = makeOscoreCnf(masterSecret, audStr);
                            claims.put(Constants.CNF, osc);
                        }
                        // DTLS profile
                        else {
                            // Make a DTLS style psk
                            CBORObject keyData = CBORObject.NewMap();
                            CBORObject coseKey = CBORObject.NewMap();

                            keyData.Add(KeyKeys.KeyType.AsCBOR(), KeyKeys.KeyType_Octet);

                            // Note: kid is the same as cti
                            byte[] kid = ctiB;
                            keyData.Add(KeyKeys.KeyId.AsCBOR(), kid);

                            SecretKey key = kg.generateKey();
                            keyData.Add(KeyKeys.Octet_K.AsCBOR(), CBORObject.FromObject(key.getEncoded()));

                            OneKey psk = new OneKey(keyData);
                            coseKey.Add(Constants.COSE_KEY, psk.AsCBOR());
                            claims.put(Constants.CNF, coseKey);
                        }
                    } catch (NoSuchAlgorithmException | CoseException e) {
                        if (!includeExi) {
                            this.cti--; // roll-back
                        } else {
                            // roll-back
                            exiSequenceNumbers.put(rsName, exiSeqNum);
                        }
                        LOGGER.severe("Message processing aborted " + "(making PSK): " + e.getMessage());
                        DhtLogger.sendLog(TYPE_ERROR, PRIO_HIGH, CAT_STATUS, DEVICE_NAME,
                                "Message processing aborted " + "(making PSK)");
                        return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
                    }

                } else if (cnf.ContainsKey(Constants.COSE_KID_CBOR)) {
                    // The client requested a specific kid

                    // Check that the kid is well-formed
                    CBORObject kidC = cnf.get(Constants.COSE_KID_CBOR);
                    if (!kidC.getType().equals(CBORType.ByteString)) {
                        if (!includeExi) {
                            this.cti--; // roll-back
                        } else {
                            // roll-back
                            exiSequenceNumbers.put(rsName, exiSeqNum);
                        }
                        LOGGER.info("Message processing aborted: " + " Malformed kid in request parameter 'cnf'");
                        DhtLogger.sendLog(TYPE_WARNING, PRIO_MEDIUM, CAT_STATUS, DEVICE_NAME,
                                "Message processing aborted: " + " Malformed kid in request parameter 'cnf'");
                        CBORObject map = CBORObject.NewMap();
                        map.Add(Constants.ERROR, Constants.INVALID_REQUEST);
                        map.Add(Constants.ERROR_DESCRIPTION, "Malformed kid in 'cnf' parameter");
                        return msg.failReply(Message.FAIL_BAD_REQUEST, map);
                    }
                    keyType = "KID";

                    // Check if the new Token is intended to update the access
                    // rights for this client
                    Set<String> ctiSet = new HashSet<>();
                    try {
                        ctiSet = this.db.getCtis4Client(id);

                    } catch (AceException e) {
                        if (!includeExi) {
                            this.cti--; // roll-back
                        } else {
                            // roll-back
                            exiSequenceNumbers.put(rsName, exiSeqNum);
                        }
                        LOGGER.severe(
                                "Message processing aborted " + "(finding cti of issues tokens): " + e.getMessage());
                        DhtLogger.sendLog(TYPE_ERROR, PRIO_HIGH, CAT_STATUS, DEVICE_NAME,
                                "Message processing aborted " + "(finding cti of issues tokens)");
                        return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
                    }

                    if (ctiSet.size() != 0) {
                        // Some Tokens have been issued to this client.

                        for (String myCti : ctiSet) {

                            // Check that not only the Token was released at
                            // some point in time, but that it is also currently
                            // stored in the Database. If so, it is possible to
                            // retrieve a non empty set of claims through its
                            // cti.
                            try {
                                if (this.db.getClaims(myCti).size() == 0) {
                                    // A Token with this cti is not active
                                    // anymore. Continue with checking the next
                                    // Token.

                                    // But first take the opportunity to clean
                                    // up some other data structures, which
                                    // might not have happened already
                                    this.cti2aud.remove(myCti);
                                    this.cti2oscId.remove(myCti);
                                    this.cti2kid.remove(myCti);

                                    continue;
                                }
                            } catch (AceException e) {
                                if (!includeExi) {
                                    this.cti--; // roll-back
                                } else {
                                    // roll-back
                                    exiSequenceNumbers.put(rsName, exiSeqNum);
                                }
                                LOGGER.severe("Message processing aborted " + "(finding previously released token): "
                                        + e.getMessage());
                                DhtLogger.sendLog(TYPE_ERROR, PRIO_HIGH, CAT_STATUS, DEVICE_NAME,
                                        "Message processing aborted " + "(finding previously released token)");
                                return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
                            }

                            String myAud = this.cti2aud.get(myCti);

                            // Check especially if the previously released Token
                            // was intended to the same Resource Server intended
                            // to consume the just requested Token
                            if (myAud != null && audStr.equals(myAud)) {

                                // Retrieve the claims of the previously
                                // released Token
                                Map<Short, CBORObject> myClaims = null;
                                try {
                                    myClaims = this.db.getClaims(myCti);
                                } catch (AceException e) {
                                    if (!includeExi) {
                                        this.cti--; // roll-back
                                    } else {
                                        // roll-back
                                        exiSequenceNumbers.put(rsName, exiSeqNum);
                                    }
                                    LOGGER.severe("Message processing aborted "
                                            + "(finding previously released token): " + e.getMessage());
                                    DhtLogger.sendLog(TYPE_ERROR, PRIO_HIGH, CAT_STATUS, DEVICE_NAME,
                                            "Message processing aborted " + "(finding previously released token)");
                                    return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
                                }

                                CBORObject oldCnf = myClaims.get(Constants.CNF);

                                if (oldCnf.get(Constants.COSE_KID) != null) {
                                    if (Arrays.equals(kidC.GetByteString(),
                                            oldCnf.get(Constants.COSE_KID).GetByteString())) {
                                        // The new Token is intended to update
                                        // access rights (not the first update
                                        // in the series)
                                        updateAccessRights = true;
                                        oldCti = new String(myCti);
                                        break;
                                    }
                                    continue;
                                }

                                // OSCORE profile
                                if (profile == Constants.COAP_OSCORE) {
                                    if (Arrays.equals(kidC.GetByteString(), oldCnf.get(Constants.OSCORE_Input_Material)
                                            .get(Constants.OS_ID).GetByteString())) {
                                        // The new Token is intended to update
                                        // access rights (first update in the
                                        // series)
                                        updateAccessRights = true;
                                        oldCti = new String(myCti);
                                        break;
                                    }
                                    continue;
                                }
                                // DTLS profile
                                else {
                                    if (Arrays.equals(kidC.GetByteString(), oldCnf.get(Constants.COSE_KEY)
                                            .get(KeyKeys.KeyId.AsCBOR()).GetByteString())) {
                                        // The new Token is intended to update
                                        // access rights (first update in the
                                        // series)
                                        updateAccessRights = true;
                                        oldCti = new String(myCti);
                                        break;
                                    }
                                    continue;
                                }
                            }
                        }

                        if (updateAccessRights == true) {
                            // The new Token is intended to update access rights

                            // OSCORE profile
                            if (profile == Constants.COAP_OSCORE) {
                                // Generate OSCORE cnf
                                CBORObject oscId = this.cti2oscId.get(oldCti);
                                if (oscId == null) {
                                    if (!includeExi) {
                                        this.cti--; // roll-back
                                    } else {
                                        // roll-back
                                        exiSequenceNumbers.put(rsName, exiSeqNum);
                                    }
                                    LOGGER.severe("Message processing aborted "
                                            + "(finding OSCORE ID when updating access rights)");
                                    DhtLogger.sendLog(TYPE_ERROR, PRIO_HIGH, CAT_STATUS, DEVICE_NAME,
                                            "Message processing aborted "
                                                    + "(finding OSCORE ID when updating access rights)");
                                    return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
                                }
                                CBORObject osc = makeOscoreCnfUpdateAccessRights(oscId);
                                claims.put(Constants.CNF, osc);
                            }
                            // DTLS profile
                            else {
                                // Make a DTLS style psk
                                CBORObject keyData = CBORObject.NewMap();
                                CBORObject coseKey = CBORObject.NewMap();

                                keyData.Add(KeyKeys.KeyType.AsCBOR(), KeyKeys.KeyType_Octet);

                                CBORObject kidCbor = this.cti2kid.get(oldCti);

                                keyData.Add(KeyKeys.KeyId.AsCBOR(), kidCbor);

                                coseKey.Add(Constants.COSE_KEY, keyData);
                                claims.put(Constants.CNF, coseKey);
                            }
                        } else {
                            LOGGER.severe("Message processing aborted "
                                    + "(cannot find access token for which access right have to be updated)");
                            DhtLogger.sendLog(TYPE_ERROR, PRIO_HIGH, CAT_STATUS, DEVICE_NAME,
                                    "Message processing aborted "
                                            + "(cannot find access token for which access right have to be updated)");
                            CBORObject myMap = CBORObject.NewMap();
                            myMap.Add(Constants.ERROR, Constants.UNSUPPORTED_POP_KEY);
                            return msg.failReply(Message.FAIL_BAD_REQUEST, myMap);
                        }
                    }

                } else {// Client has provided a key
                    // Check what key the client provided
                    OneKey key = null;
                    try {
                        key = getKey(cnf, id);
                    } catch (AceException | CoseException e) {
                        if (!includeExi) {
                            this.cti--; // roll-back
                        } else {
                            // roll-back
                            exiSequenceNumbers.put(rsName, exiSeqNum);
                        }
                        LOGGER.severe("Message processing aborted: " + e.getMessage());
                        DhtLogger.sendLog(TYPE_ERROR, PRIO_HIGH, CAT_STATUS, DEVICE_NAME, "Message processing aborted");
                        if (e.getMessage().startsWith("Malformed")) {
                            CBORObject map = CBORObject.NewMap();
                            map.Add(Constants.ERROR, Constants.INVALID_REQUEST);
                            map.Add(Constants.ERROR_DESCRIPTION, "Malformed 'cnf' parameter in request");
                            return msg.failReply(Message.FAIL_BAD_REQUEST, map);
                        }
                        return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
                    }
                    if (key == null) {
                        if (!includeExi) {
                            this.cti--; // roll-back
                        } else {
                            // roll-back
                            exiSequenceNumbers.put(rsName, exiSeqNum);
                        }
                        CBORObject map = CBORObject.NewMap();
                        map.Add(Constants.ERROR, Constants.INVALID_REQUEST);
                        map.Add(Constants.ERROR_DESCRIPTION, "Couldn't retrieve RPK");
                        LOGGER.log(Level.INFO, "Message processing aborted: " + "Couldn't retrieve RPK");
                        DhtLogger.sendLog(TYPE_WARNING, PRIO_MEDIUM, CAT_STATUS, DEVICE_NAME,
                                "Message processing aborted: " + "Couldn't retrieve RPK");
                        return msg.failReply(Message.FAIL_BAD_REQUEST, map);
                    }

                    if (key.get(KeyKeys.KeyType).equals(KeyKeys.KeyType_Octet)) {
                        // Client tried to submit a symmetric key => reject
                        if (!includeExi) {
                            this.cti--; // roll-back
                        } else {
                            // roll-back
                            exiSequenceNumbers.put(rsName, exiSeqNum);
                        }
                        CBORObject map = CBORObject.NewMap();
                        map.Add(Constants.ERROR, Constants.INVALID_REQUEST);
                        map.Add(Constants.ERROR_DESCRIPTION, "Client tried to provide cnf PSK");
                        LOGGER.log(Level.INFO, "Message processing aborted: " + "Client tried to provide cnf PSK");
                        DhtLogger.sendLog(TYPE_WARNING, PRIO_MEDIUM, CAT_STATUS, DEVICE_NAME,
                                "Message processing aborted: " + "Client tried to provide cnf PSK");
                        return msg.failReply(Message.FAIL_BAD_REQUEST, map);
                    }

                    // At this point we assume the client wants to use RPK
                    keyType = "RPK";

                    // Check that the client used this RPK to create this
                    // session
                    try {
                        RawPublicKeyIdentity rpkId = new RawPublicKeyIdentity(key.AsPublicKey());
                        if (!rpkId.getName().equals(id)) {
                            if (!includeExi) {
                                this.cti--; // roll-back
                            } else {
                                // roll-back
                                exiSequenceNumbers.put(rsName, exiSeqNum);
                            }
                            CBORObject map = CBORObject.NewMap();
                            map.Add(Constants.ERROR, Constants.UNSUPPORTED_POP_KEY);
                            LOGGER.log(Level.INFO, "Message processing aborted: " + "Client used unauthenticated RPK");
                            DhtLogger.sendLog(TYPE_WARNING, PRIO_MEDIUM, CAT_STATUS, DEVICE_NAME,
                                    "Message processing aborted: " + "Client used unauthenticated RPK");
                            return msg.failReply(Message.FAIL_BAD_REQUEST, map);
                        }

                    } catch (CoseException e) {
                        if (!includeExi) {
                            this.cti--; // roll-back
                        } else {
                            // roll-back
                            exiSequenceNumbers.put(rsName, exiSeqNum);
                        }
                        CBORObject map = CBORObject.NewMap();
                        map.Add(Constants.ERROR, Constants.UNSUPPORTED_POP_KEY);
                        LOGGER.log(Level.INFO, "Message processing aborted: " + "Unsupported pop key type RPK");
                        DhtLogger.sendLog(TYPE_WARNING, PRIO_MEDIUM, CAT_STATUS, DEVICE_NAME,
                                "Message processing aborted: " + "Unsupported pop key type RPK");
                        LOGGER.log(Level.FINEST, e.getMessage());
                        return msg.failReply(Message.FAIL_BAD_REQUEST, map);
                    }

                    // Can the audience support this?
                    try {
                        if (!isSupported(keyType, aud)) {
                            if (!includeExi) {
                                this.cti--; // roll-back
                            } else {
                                // roll-back
                                exiSequenceNumbers.put(rsName, exiSeqNum);
                            }
                            CBORObject map = CBORObject.NewMap();
                            map.Add(Constants.ERROR, Constants.UNSUPPORTED_POP_KEY);
                            LOGGER.log(Level.INFO, "Message processing aborted: " + "Unsupported pop key type RPK");
                            DhtLogger.sendLog(TYPE_WARNING, PRIO_MEDIUM, CAT_STATUS, DEVICE_NAME,
                                    "Message processing aborted: " + "Unsupported pop key type RPK");
                            return msg.failReply(Message.FAIL_BAD_REQUEST, map);
                        }
                    } catch (AceException e) {
                        if (!includeExi) {
                            this.cti--; // roll-back
                        } else {
                            // roll-back
                            exiSequenceNumbers.put(rsName, exiSeqNum);
                        }
                        LOGGER.severe("Message processing aborted: " + e.getMessage());
                        DhtLogger.sendLog(TYPE_ERROR, PRIO_HIGH, CAT_STATUS, DEVICE_NAME, "Message processing aborted");
                        return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
                    }

                    // Audience support RPK, use provided RPK
                    CBORObject coseKey = CBORObject.NewMap();
                    coseKey.Add(Constants.COSE_KEY, key.AsCBOR());
                    claims.put(Constants.CNF, coseKey);
                }
                break;
            case Constants.PROFILE:
                claims.put(Constants.PROFILE, CBORObject.FromObject(profile));
                break;
            default:
                LOGGER.severe("Unknown claim type in /token endpoint configuration: " + c);
                DhtLogger.sendLog(TYPE_ERROR, PRIO_HIGH, CAT_STATUS, DEVICE_NAME,
                        "Unknown claim type in /token endpoint configuration: " + c);
                return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
            }
        }

        AccessToken token = null;
        try {
            token = AccessTokenFactory.generateToken(tokenType, claims);
        } catch (AceException e) {
            if (!includeExi) {
                this.cti--; // roll-back
            } else {
                // roll-back
                exiSequenceNumbers.put(rsName, exiSeqNum);
            }

            // If the OSCORE profile is used, and this was a first-released
            // Token to this client for RS in question, roll-back the counter
            // used for the 'id' parameter in the OSCORE Security Context and
            // the Id Context value assigned for this Resource Server
            if (profile == Constants.COAP_OSCORE && updateAccessRights == false) {
                this.OSCORE_material_counter--;
                if (this.idContextInfoMap.containsKey(audStr)) {
                    this.idContextInfoMap.get(audStr).rollback();
                }
            }

            LOGGER.severe("Message processing aborted: " + e.getMessage());
            DhtLogger.sendLog(TYPE_ERROR, PRIO_HIGH, CAT_STATUS, DEVICE_NAME, "Message processing aborted");
            return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
        }

        CBORObject rsInfo = CBORObject.NewMap();
        try {

            boolean includeProfile = false;

            if (!this.db.hasDefaultProfile(id)) {
                // This client supports multiple profiles; need to specify the
                // exact one to use
                includeProfile = true;
            } else {
                CBORObject profileParameter = msg.getParameter(Constants.PROFILE);
                if (profileParameter != null && profileParameter.equals(CBORObject.Null)) {
                    // The client has requested an explicit indication of the
                    // profile to use
                    includeProfile = true;
                }
            }

            if (includeProfile == true) {
                rsInfo.Add(Constants.PROFILE, CBORObject.FromObject(profile));
            }
            // Otherwise, no need to explicitly indicate the used profile

        } catch (AceException e) {
            if (!includeExi) {
                this.cti--; // roll-back
            } else {
                // roll-back
                exiSequenceNumbers.put(rsName, exiSeqNum);
            }

            // If the OSCORE profile is used, and this was a first-released
            // Token to this client for RS in question, roll-back the counter
            // used for the 'id' parameter in the OSCORE Security Context and
            // the Id Context value assigned for this Resource Server
            if (profile == Constants.COAP_OSCORE && updateAccessRights == false) {
                this.OSCORE_material_counter--;
                if (this.idContextInfoMap.containsKey(audStr)) {
                    this.idContextInfoMap.get(audStr).rollback();
                }
            }

            LOGGER.severe("Message processing aborted: " + e.getMessage());
            DhtLogger.sendLog(TYPE_ERROR, PRIO_HIGH, CAT_STATUS, DEVICE_NAME, "Message processing aborted");
            return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
        }

        if (keyType != null && keyType.equals("PSK")) {
            if (profile == Constants.COAP_OSCORE) {

                if (updateAccessRights == false) {
                    rsInfo.Add(Constants.CNF, claims.get(Constants.CNF));
                }
                // Do not add 'cnf' if the OSCORE profile is used and
                // the Token is released for updating access rights

            } else {
                rsInfo.Add(Constants.CNF, claims.get(Constants.CNF));
            }
        } else if (keyType != null && keyType.equals("RPK")) {
            Set<CBORObject> rscnfs = new HashSet<>();
            try {
                rscnfs = makeRsCnf(aud);
            } catch (AceException e) {
                if (!includeExi) {
                    this.cti--; // roll-back
                } else {
                    // roll-back
                    exiSequenceNumbers.put(rsName, exiSeqNum);
                }

                // If the OSCORE profile is used, and this was a first-released
                // Token to this client for RS in question, roll-back the
                // counter used for the 'id' parameter in the OSCORE Security
                // Context and the Id Context value assigned for this Resource
                // Server
                if (profile == Constants.COAP_OSCORE && updateAccessRights == false) {
                    this.OSCORE_material_counter--;
                    if (this.idContextInfoMap.containsKey(audStr)) {
                        this.idContextInfoMap.get(audStr).rollback();
                    }
                }

                LOGGER.severe("Message processing aborted: " + e.getMessage());
                DhtLogger.sendLog(TYPE_ERROR, PRIO_HIGH, CAT_STATUS, DEVICE_NAME, "Message processing aborted");
                return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
            }
            for (CBORObject rscnf : rscnfs) {
                rsInfo.Add(Constants.RS_CNF, rscnf);
            }
        } // Skip cnf if client requested specific KID.

        // Handle "scope" both as String and as Byte Array
        if (scope instanceof String && !allowedScopes.equals(scope)) {
            rsInfo.Add(Constants.SCOPE, CBORObject.FromObject(allowedScopes));
        }
        if (scope instanceof byte[] && !(Arrays.equals((byte[]) allowedScopes, (byte[]) scope))) {
            rsInfo.Add(Constants.SCOPE, CBORObject.FromObject(allowedScopes));
        }

        if (token instanceof CWT) {

            CwtCryptoCtx ctx = null;
            try {
                ctx = EndpointUtils.makeCommonCtx(aud, this.db, this.privateKey, sign);
            } catch (AceException | CoseException e) {
                if (!includeExi) {
                    this.cti--; // roll-back
                } else {
                    // roll-back
                    exiSequenceNumbers.put(rsName, exiSeqNum);
                }

                // If the OSCORE profile is used, and this was a first-released
                // Token to this client for RS in question, roll-back the
                // counter used for the 'id' parameter in the OSCORE Security
                // Context and the Id Context value assigned for this Resource
                // Server
                if (profile == Constants.COAP_OSCORE && updateAccessRights == false) {
                    this.OSCORE_material_counter--;
                    if (this.idContextInfoMap.containsKey(audStr)) {
                        this.idContextInfoMap.get(audStr).rollback();
                    }
                }

                LOGGER.severe("Message processing aborted: " + e.getMessage());
                DhtLogger.sendLog(TYPE_ERROR, PRIO_HIGH, CAT_STATUS, DEVICE_NAME, "Message processing aborted");
                return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
            }
            if (ctx == null) {
                if (!includeExi) {
                    this.cti--; // roll-back
                } else {
                    // roll-back
                    exiSequenceNumbers.put(rsName, exiSeqNum);
                }

                // If the OSCORE profile is used, and this was a first-released
                // Token to this client for RS in question, roll-back the
                // counter used for the 'id' parameter in the OSCORE Security
                // Context and the Id Context value assigned for this Resource
                // Server
                if (profile == Constants.COAP_OSCORE && updateAccessRights == false) {
                    this.OSCORE_material_counter--;
                    if (this.idContextInfoMap.containsKey(audStr)) {
                        this.idContextInfoMap.get(audStr).rollback();
                    }
                }

                CBORObject map = CBORObject.NewMap();
                map.Add(Constants.ERROR, "No common security context found for audience");
                LOGGER.log(Level.INFO, "Message processing aborted: No common security context found for audience");
                DhtLogger.sendLog(TYPE_WARNING, PRIO_MEDIUM, CAT_STATUS, DEVICE_NAME,
                        "Message processing aborted: No common security context found for audience");
                return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, map);
            }
            CWT cwt = (CWT) token;
            Map<HeaderKeys, CBORObject> uHeaders = null;
            if (this.setAudHeader) {
                // Add the audience as the KID in the header, so it can be
                // referenced by introspection requests.
                CBORObject requestedAud = CBORObject.NewArray();
                for (String a : aud) {
                    requestedAud.Add(a);
                }
                uHeaders = new HashMap<>();
                uHeaders.put(HeaderKeys.KID, requestedAud);
            }
            try {
                rsInfo.Add(Constants.ACCESS_TOKEN, cwt.encode(ctx, null, uHeaders).EncodeToBytes());
            } catch (IllegalStateException | InvalidCipherTextException | CoseException | AceException e) {
                if (!includeExi) {
                    this.cti--; // roll-back
                } else {
                    // roll-back
                    exiSequenceNumbers.put(rsName, exiSeqNum);
                }

                // If the OSCORE profile is used, and this was a first-released
                // Token to this client for RS in question, roll-back the
                // counter used for the 'id' parameter in the OSCORE Security
                // Context and the Id Context value assigned for this Resource
                // Server
                if (profile == Constants.COAP_OSCORE && updateAccessRights == false) {
                    this.OSCORE_material_counter--;
                    if (this.idContextInfoMap.containsKey(audStr)) {
                        this.idContextInfoMap.get(audStr).rollback();
                    }
                }

                LOGGER.severe("Message processing aborted: " + e.getMessage());
                DhtLogger.sendLog(TYPE_ERROR, PRIO_HIGH, CAT_STATUS, DEVICE_NAME, "Message processing aborted");
                return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
            }
        } else {
            rsInfo.Add(Constants.ACCESS_TOKEN, token.encode().EncodeToBytes());
        }

        try {

            // If the claim set includes EXI but not EXP, then extend the claim
            // set to be stored as follows:
            //
            // 1. Add an EXP claim, computed as current time plus the EXI value.
            // This allows to purge the token if expired, even though it was
            // created without the EXP claim.
            //
            // 2. Add an internal "sentinel claim" to signal the presence of the
            // artificially added EXP claim.
            // In case of introspection, this allows the Authorization Server to
            // return the Access Token like it was originally issued, i.e.,
            // without the EXI claim if this was artificially added.
            if (claims.containsKey(Constants.EXI) && !claims.containsKey(Constants.EXP)) {

                Long now = this.time.getCurrentTime();
                Long exp = now + claims.get(Constants.EXI).AsNumber().ToInt64Checked();

                claims.put(Constants.EXP, CBORObject.FromObject(exp));

                // Add the "sentinel claim"
                claims.put(Constants.LATE_ADDED_EXP, CBORObject.True);
            }

            this.db.addToken(ctiStr, claims);
            this.db.addCti2Client(ctiStr, id);
            if (!includeExi) {
                this.db.saveCtiCounter(this.cti);
            } else {
                this.db.saveExiSequenceNumber(exiSeqNum + 1, rsName);
            }

            // In case the client has asked to use a PSK, store further
            // associations, to support the issuing of Access Tokens for
            // updating access rights
            if (keyType != null && keyType.equals("PSK")) {

                this.cti2aud.put(ctiStr, audStr);

                if (profile == Constants.COAP_OSCORE) {
                    CBORObject oscId;
                    if (updateAccessRights == false) {
                        // The Token is not updating access rights, hence the
                        // identifier of the OSCORE Input Material is the 'id'
                        // 'OSCORE_Input_Material' element of the 'cnf' claim
                        oscId = claims.get(Constants.CNF).get(Constants.OSCORE_Input_Material).get(Constants.OS_ID);
                    } else {
                        // The Token is updating access rights, hence the
                        // identifier of the
                        // OSCORE Input Material is used as 'kid' in the 'cnf'
                        // claim of the Token
                        oscId = claims.get(Constants.CNF).get(Constants.COSE_KID_CBOR);
                    }

                    // A deep copy is needed
                    byte[] oscIdCopy = Arrays.copyOf(oscId.GetByteString(), oscId.GetByteString().length);
                    this.cti2oscId.put(ctiStr, CBORObject.FromObject(oscIdCopy));

                } else if (profile == Constants.COAP_DTLS) {
                    // Regardless if the Token is updating access rights or not,
                    // the identifier of the PoP key is the 'kid' parameter
                    // inside the 'COSE_Key' parameter of the 'cnf' claim
                    CBORObject kid = claims.get(Constants.CNF).get(Constants.COSE_KEY).get(KeyKeys.KeyId.AsCBOR());

                    // A deep copy is needed
                    byte[] kidCopy = Arrays.copyOf(kid.GetByteString(), kid.GetByteString().length);
                    this.cti2kid.put(ctiStr, CBORObject.FromObject(kidCopy));

                }

                // The just issued Token is updating access rights, hence delete
                // the superseded Token
                if (updateAccessRights == true) {
                    removeToken(oldCti);
                }

            }

        } catch (AceException e) {
            if (!includeExi) {
                this.cti--; // roll-back
            } else {
                // roll-back
                exiSequenceNumbers.put(rsName, exiSeqNum);
            }

            this.cti2aud.remove(ctiStr);

            if (keyType != null && keyType.equals("PSK")) {

                if (profile == Constants.COAP_OSCORE) {
                    if (updateAccessRights == false) {
                        // Roll-back the counter used for the 'id' parameter in
                        // the OSCORE Security Context and the Id Context value
                        // assigned for this Resource Server
                        this.OSCORE_material_counter--;
                        if (this.idContextInfoMap.containsKey(audStr)) {
                            this.idContextInfoMap.get(audStr).rollback();
                        }
                    }

                    this.cti2oscId.remove(ctiStr);
                } else if (profile == Constants.COAP_DTLS) {
                    this.cti2kid.remove(ctiStr);
                }

            }

            LOGGER.severe("Message processing aborted: " + e.getMessage());
            DhtLogger.sendLog(TYPE_ERROR, PRIO_HIGH, CAT_STATUS, DEVICE_NAME, "Message processing aborted");
            return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
        }
        LOGGER.log(Level.INFO, "Returning token: " + ctiStr);
        DhtLogger.sendLog(TYPE_INFO, PRIO_LOW, CAT_STATUS, DEVICE_NAME, "Returning token. " + "[ctiStr: " + ctiStr
                + ". " + "rsName: " + rsName + ". " + "audStr: " + audStr + ". " + "id: " + id + "]");

        // ctiStr in base64, rsName, audStr, id
        //
        // If the EXP claim was added after the actual creation of the Access
        // Token, then print all the claims except for EXP and the sentinel
        // claim.
        if (claims.containsKey(Constants.LATE_ADDED_EXP)) {
            Map<Short, CBORObject> actualClaims = new HashMap<>();
            for (Short s : claims.keySet()) {
                actualClaims.put(s, claims.get(s));
            }
            LOGGER.log(Level.FINEST, "Claims: " + actualClaims.toString());
        }
        return msg.successReply(Message.CREATED, rsInfo);
    }

    /**
     * Populate RS_CNF
     * 
     * @throws AceException
     */
    private Set<CBORObject> makeRsCnf(Set<String> aud) throws AceException {
        Set<String> rss = new HashSet<>();
        Set<CBORObject> rscnfs = new HashSet<>();
        for (String audE : aud) {
            rss.addAll(this.db.getRSS(audE));
        }
        for (String rs : rss) {
            OneKey rsKey = this.db.getRsRPK(rs);
            CBORObject rscnf = CBORObject.NewMap();
            rscnf.Add(Constants.COSE_KEY_CBOR, rsKey.AsCBOR());
            rscnfs.add(rscnf);

        }
        return rscnfs;
    }

    /**
     * Create the value of a 'cnf' claim as an "OSCORE_Input_Material" CBOR object.
     * 
     * @param masterSecret the OSCORE Master Secret
     * @param rsName the name of the Resource Server
     * 
     * @return the value of a 'cnf' claim as an "OSCORE_Input_Material" CBOR object
     */
    synchronized private CBORObject makeOscoreCnf(byte[] masterSecret, String rsName) {
        CBORObject osccnf = CBORObject.NewMap();
        CBORObject osc = CBORObject.NewMap();

        osc.Add(Constants.OS_MS, masterSecret);

        osc.Add(Constants.OS_ID, Util.intToBytes(OSCORE_material_counter));
        OSCORE_material_counter++;

        if (masterSaltSize != 0) {
            byte[] masterSalt = new byte[masterSaltSize];
            new SecureRandom().nextBytes(masterSalt);
            osc.Add(Constants.OS_SALT, masterSalt);
        }

        if (this.provideIdContext == true) {

            IdContextInfo idContextInfo;
            if (this.idContextInfoMap.containsKey(rsName)) {
                idContextInfo = this.idContextInfoMap.get(rsName);
            } else {
                // This is the first Access Token for this Resource Server
                idContextInfo = new IdContextInfo();
                this.idContextInfoMap.put(rsName, idContextInfo);
            }

            byte[] idContext = idContextInfo.getIdContext();
            osc.Add(Constants.OS_CONTEXTID, idContext);

        }

        osccnf.Add(Constants.OSCORE_Input_Material, osc);
        return osccnf;
    }

    /**
     * Create the value of a 'cnf' claim as a "kid" CBOR object.
     * 
     * @param oscId the Identifier of the OSCORE Input Material object
     * 
     * @return the value of a 'cnf' claim as a "kid" CBOR object
     */
    private CBORObject makeOscoreCnfUpdateAccessRights(CBORObject oscId) {
        CBORObject osccnf = CBORObject.NewMap();

        osccnf.Add(Constants.COSE_KID_CBOR, oscId);
        return osccnf;
    }

    /**
     * Process an authorization grant message
     * 
     * @param msg the message
     * 
     * @return the reply
     */
    private Message processAC(Message msg) {
        // 3. Check if the request has a grant
        CBORObject cbor = msg.getParameter(Constants.CODE);
        if (cbor == null) {
            CBORObject map = CBORObject.NewMap();
            map.Add(Constants.ERROR, Constants.INVALID_REQUEST);
            map.Add(Constants.ERROR_DESCRIPTION, "No code found for message");
            LOGGER.log(Level.INFO, "Message processing aborted: " + "No code found for message");
            return msg.failReply(Message.FAIL_BAD_REQUEST, map);
        }
        if (!cbor.getType().equals(CBORType.TextString)) {
            CBORObject map = CBORObject.NewMap();
            map.Add(Constants.ERROR, Constants.INVALID_REQUEST);
            map.Add(Constants.ERROR_DESCRIPTION, "Invalid grant format");
            LOGGER.log(Level.INFO, "Message processing aborted: " + "Invalid grant format");
            return msg.failReply(Message.FAIL_BAD_REQUEST, map);
        }
        String code = cbor.AsString();

        // 4. Check if grant valid and unused
        try {
            if (!this.db.isGrantValid(code)) {
                CBORObject map = CBORObject.NewMap();
                map.Add(Constants.ERROR, Constants.INVALID_GRANT);
                LOGGER.log(Level.INFO, "Message processing aborted: " + "Invalid grant");
                return msg.failReply(Message.FAIL_BAD_REQUEST, map);
            }
        } catch (AceException e) {
            LOGGER.log(Level.SEVERE, "Message processing aborted " + "(checking grant): " + e.getMessage());
            return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
        }

        // 5. Mark grant invalid
        try {
            this.db.useGrant(code);
        } catch (AceException e) {
            LOGGER.log(Level.SEVERE, "Message processing aborted " + "(marking grant invalid): " + e.getMessage());
            return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
        }

        // 6. Return the RS Information
        CBORObject rsInfo = CBORObject.NewMap();

        try {
            Map<Short, CBORObject> rsInfoDB = this.db.getRsInfo(code);
            for (Map.Entry<Short, CBORObject> e : rsInfoDB.entrySet()) {
                rsInfo.Add(e.getKey(), e.getValue());
            }
        } catch (AceException e) {
            LOGGER.log(Level.SEVERE, "Message processing aborted " + "(collecting RS Info" + e.getMessage());
            return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
        }

        if (rsInfo == null || !rsInfo.getType().equals(CBORType.Map)) {
            LOGGER.log(Level.SEVERE, "Message processing aborted: " + "no RS information found for grant: " + code);
            CBORObject map = CBORObject.NewMap();
            map.Add(Constants.ERROR, Constants.INVALID_GRANT);
            map.Add(Constants.ERROR_DESCRIPTION, "No token found for grant");
            return msg.failReply(Message.FAIL_BAD_REQUEST, map);
        }
        return msg.successReply(Message.CREATED, rsInfo);
    }

    private boolean isSupported(String keyType, Set<String> aud) throws AceException {
        Set<String> keyTypes = this.db.getSupportedPopKeyTypes(aud);
        return keyTypes.contains(keyType);
    }

    /**
     * Retrieves a key from a cnf structure.
     * 
     * @param cnf the cnf structure
     * 
     * @return the key
     * 
     * @throws AceException
     * @throws CoseException
     */
    private OneKey getKey(CBORObject cnf, String id) throws AceException, CoseException {
        CBORObject crpk = null;
        if (cnf.ContainsKey(Constants.COSE_KEY_CBOR)) {
            crpk = cnf.get(Constants.COSE_KEY_CBOR);
            if (crpk == null) {
                return null;
            }
            return new OneKey(crpk);
        } else if (cnf.ContainsKey(Constants.COSE_ENCRYPTED_CBOR)) {
            Encrypt0Message msg = new Encrypt0Message();
            CBORObject encC = cnf.get(Constants.COSE_ENCRYPTED_CBOR);
            try {
                msg.DecodeFromCBORObject(encC);
                OneKey psk = this.db.getCPSK(id);
                if (psk == null) {
                    LOGGER.severe("Couldn't find a key to decrypt cnf parameter");
                    throw new AceException("No key found to decrypt cnf parameter");
                }
                CBORObject key = psk.get(KeyKeys.Octet_K);
                if (key == null || !key.getType().equals(CBORType.ByteString)) {
                    LOGGER.severe("Corrupt key retrieved from database");
                    throw new AceException("Key error in the database");
                }
                msg.decrypt(key.GetByteString());
                CBORObject keyData = CBORObject.DecodeFromBytes(msg.GetContent());
                return new OneKey(keyData);
            } catch (CoseException e) {
                LOGGER.severe("Error while decrypting a cnf claim: " + e.getMessage());
                throw new AceException("Error while decrypting a cnf parameter");
            }
        } // Note: We checked the COSE_KID_CBOR case before
        throw new AceException("Malformed cnf structure");
    }

    /**
     * Removes a token from the registry
     * 
     * @param cti the token identifier Base64 encoded
     * @throws AceException
     */
    public void removeToken(String cti) throws AceException {
        this.db.deleteToken(cti);

        this.cti2aud.remove(cti);
        this.cti2oscId.remove(cti);
        this.cti2kid.remove(cti);

        // FIXME: Add the token to the TRL
    }

    @Override
    public void close() throws AceException {
        this.db.saveCtiCounter(this.cti);

        for (String rs : exiSequenceNumbers.keySet())
            this.db.saveExiSequenceNumber(exiSequenceNumbers.get(rs).intValue(), rs);

        this.db.close();
    }

    /**
     * Relevant only when the OSCORE profile is used
     * 
     * An instance of this class tracks the status of OSCORE Id Contexts assigned to a Resource Server
     */
    class IdContextInfo {

        short currentSize;
        int currentValue;

        public IdContextInfo() {
            currentSize = 1;
            currentValue = 0;
        }

        // Retrieve the next unassigned IdContext for this Resource Server,
        // using the smallest possible size in bytes. That is, first consume all
        // the Id Contexts of 1 byte in size, then all the Id Contexts of 2
        // bytes in size, and so on up to 4 bytes in size.
        synchronized public byte[] getIdContext() {

            // Check if the size has to be changed
            switch (currentSize) {

            case 1: // Max value: 2^8 - 1
            case 2: // Max value: 2^16 - 1
            case 3: // Max value: 2^24 - 1
                if (currentValue == ((1 << (currentSize * 8)) - 1)) {
                    currentSize++;
                    currentValue = 0;
                }
                break;
            case 4: // Max value: 2^31 - 1 --- The other half is for negative
                    // integers
                if (currentValue == ((1 << ((currentSize * 8) - 1)) - 1)) {
                    currentSize = 1;
                    currentValue = 0;
                }
                break;
            default:
                return null;
            }

            byte[] idContext = null;
            switch (currentSize) {
            case 1:
                idContext = new byte[] { (byte) (currentValue) };
                break;
            case 2:
                idContext = new byte[] { (byte) (currentValue >>> 8), (byte) currentValue };
                break;
            case 3:
                idContext = new byte[] { (byte) (currentValue >>> 16), (byte) (currentValue >>> 8),
                                        (byte) currentValue };
                break;
            case 4:
                idContext = new byte[] { (byte) (currentValue >>> 24), (byte) (currentValue >>> 16),
                                        (byte) (currentValue >>> 8), (byte) currentValue };
                break;
            }

            currentValue++;
            return idContext;

        }

        // Free up the Id Context latest assigned for this Resource Server
        synchronized public void rollback() {

            if (currentValue != 0) {
                currentValue--;
            } else {
                switch (currentSize) {
                case 1: // Restore the maximum value: 2^31 - 1 --- The other
                        // half is for negative integers
                    currentSize = 4;
                    currentValue = (1 << ((currentSize * 8) - 1)) - 1;
                    break;
                case 2: // Restore the maximum value: 2^8 - 1
                case 3: // Restore the maximum value: 2^16 - 1
                case 4: // Restore the maximum value: 2^24 - 1
                    currentSize--;
                    currentValue = (1 << (currentSize * 8)) - 1;
                    break;
                }
            }
        }

    }

}