Token.java
/*******************************************************************************
* Copyright (c) 2025, RISE AB
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/
package se.sics.ace.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.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());
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: " + 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: " + 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: " + id);
return msg.failReply(Message.FAIL_UNAUTHORIZED, map);
}
} catch (AceException e) {
LOGGER.severe("Database error: "
+ e.getMessage());
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");
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);
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());
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");
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");
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());
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");
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");
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());
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");
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());
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");
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());
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");
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());
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());
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");
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");
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());
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());
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");
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());
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());
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'");
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());
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());
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());
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)");
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)");
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("1Message processing aborted: "
+ e.getMessage());
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");
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");
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");
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");
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");
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("2Message processing aborted: "
+ e.getMessage());
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);
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("3Message processing aborted: "
+ e.getMessage());
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("4Message processing aborted: "
+ e.getMessage());
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("5Message processing aborted: "
+ e.getMessage());
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("6Message processing aborted: "
+ e.getMessage());
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");
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("7Message processing aborted: "
+ e.getMessage());
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("8Message processing aborted: " + e.getMessage());
return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
}
LOGGER.log(Level.INFO, "Returning token: " + ctiStr);
// 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;
}
}
}
}
}