CoapDeliverer.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.coap.rs;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.logging.Logger;
import org.eclipse.californium.core.coap.CoAP.ResponseCode;
import org.eclipse.californium.core.coap.Request;
import org.eclipse.californium.core.coap.Response;
import org.eclipse.californium.core.network.CoapEndpoint;
import org.eclipse.californium.core.network.Exchange;
import org.eclipse.californium.core.server.MessageDeliverer;
import org.eclipse.californium.core.server.ServerMessageDeliverer;
import org.eclipse.californium.core.server.resources.Resource;
import org.eclipse.californium.elements.Connector;
import org.eclipse.californium.elements.DtlsEndpointContext;
import org.eclipse.californium.elements.EndpointContext;
import org.eclipse.californium.scandium.DTLSConnector;
import com.upokecenter.cbor.CBORException;
import com.upokecenter.cbor.CBORObject;
import com.upokecenter.cbor.CBORType;
import org.eclipse.californium.cose.KeyKeys;
import se.sics.ace.AceException;
import se.sics.ace.Constants;
import se.sics.ace.coap.CoapReq;
import se.sics.ace.rs.AsRequestCreationHints;
import se.sics.ace.rs.IntrospectionException;
import se.sics.ace.rs.IntrospectionHandler;
import se.sics.ace.rs.TokenRepository;
/**
* This deliverer processes incoming and outgoing messages at the RS
* according to the specifications of the ACE framework (RFC 9200)
*
* It can handle tokens passed through the DTLS handshake as specified in Section 3.3.2 of RFC 9202.
*
* It's specific task is to match requests against existing access tokens
* to see if the request is authorized.
*
* @author Ludwig Seitz and Marco Tiloca
*
*/
public class CoapDeliverer implements MessageDeliverer {
/**
* The logger
*/
private static final Logger LOGGER
= Logger.getLogger(CoapDeliverer.class.getName());
/**
* The introspection handler
*/
private IntrospectionHandler i;
/**
* The class managing the AS Request Creation Hints
*/
private AsRequestCreationHints asRCH;
/**
* CoAP Endpoint used by the Resource Server for secure communication with the Clients.
* This is required in order to be able to terminate secure associations with a Client
* upon deleting the last access token bound to that association. This is the case, e.g.,
* for access tokens bound to a DTLS connection.
*/
private CoapEndpoint coapEndpoint;
/**
* The ServerMessageDeliverer that processes the request
* after access control has been done
*/
private ServerMessageDeliverer d;
/**
* Constructor.
*
* Note: This expects that a TokenRepository has been created.
*
* @param root the root of the resources that this deliverer controls
* @param i the introspection handler or null if there isn't any.
* @param asRCHM the AS Request Creation Hints Manager.
* @param cep the CoAP endpoint used by the Resource Server for secure communication with the Clients
* @throws AceException if the token repository is not initialized
*/
public CoapDeliverer(Resource root, IntrospectionHandler i, AsRequestCreationHints asRCHM, CoapEndpoint cep) throws AceException {
if (TokenRepository.getInstance() == null) {
throw new AceException("Must initialize TokenRepository");
}
this.d = new ServerMessageDeliverer(root);
this.asRCH = asRCHM;
if (cep == null) {
throw new AceException("Null CoAP Endpoint when setting up the CoAPDeliverer");
}
this.coapEndpoint = cep;
}
//Really the TokenRepository _should not_ be closed here
@SuppressWarnings("resource")
@Override
public void deliverRequest(final Exchange ex) {
Request request = ex.getCurrentRequest();
Response r = null;
//authz-info is not under access control
try {
URI uri = new URI(request.getURI());
//Need to check with and without trailing / in case there are query options
if (uri.getPath().endsWith("/authz-info") || uri.getPath().endsWith("/authz-info/") ) {
this.d.deliverRequest(ex);
return;
}
} catch (URISyntaxException e) {
LOGGER.warning("Request-uri " + request.getURI()
+ " is invalid: " + e.getMessage());
r = new Response(ResponseCode.BAD_REQUEST);
ex.sendResponse(r);
return;
}
String subject = null;
if (request.getSourceContext() == null
|| request.getSourceContext().getPeerIdentity() == null) {
Request req = ex.getRequest();
try {
subject = CoapReq.getInstance(req, false).getSenderId();
if (subject == null) {
LOGGER.warning("Unauthenticated client tried to get access");
failUnauthz(ex);
return;
}
} catch (AceException e) {
LOGGER.severe("Error while retrieving the client identity: " + e.getMessage());
}
} else {
subject = request.getSourceContext().getPeerIdentity().getName();
}
TokenRepository tr = TokenRepository.getInstance();
if (tr == null) {
LOGGER.finest("TokenRepository not initialized");
ex.sendResponse(new Response(
ResponseCode.INTERNAL_SERVER_ERROR));
}
String kid = TokenRepository.getInstance().getKid(subject);
if (kid == null) {//Check if this was the Base64 encoded kid map
try {
CBORObject cbor = CBORObject.DecodeFromBytes(
Base64.getDecoder().decode(subject));
if (cbor.getType().equals(CBORType.Map)) {
CBORObject ckid = cbor.get(KeyKeys.KeyId.AsCBOR());
if (ckid != null && ckid.getType().equals(
CBORType.ByteString)) {
kid = new String(ckid.GetByteString(),
Constants.charset);
} else { //No kid in that CBOR map or it isn't a bstr
failUnauthz(ex);
return;
}
} else {//Some weird CBOR that is not a map here
failUnauthz(ex);
return;
}
} catch (CBORException e) {//Really no kid found for that subject
LOGGER.finest("Error while trying to parse some "
+ "subject identity to CBOR: " + e.getMessage());
failUnauthz(ex);
return;
} catch (IllegalArgumentException e) {//Text was not Base64 encoded
LOGGER.finest("Error: " + e.getMessage()
+ " while trying to Base64 decode this: " + subject);
failUnauthz(ex);
return;
}
}
String resource = request.getOptions().getUriPathString();
short action = (short) request.getCode().value;
try {
int res = TokenRepository.getInstance().canAccess(
kid, subject, resource, action, this.i);
// In case an error response is returned, it will be a Request Creation Hints message.
//
// The message will include 'kid' as the "key identifier of a key used in the
// existing security association between the client and the RS". Note that:
//
// - For the DTLS profile, this is already what the RS stores as 'kid'
//
// - For the OSCORE profile, this has to actually be the identifier of
// the OSCORE Input Material, which has to be separately retrieved
// Check if the security association was an OSCORE Security Context
if (tr.getOscoreId(subject) != null) {
// The 'kid' included in the Creation Hints message will
// will be the identifier of the OSCORE Input Material
kid = tr.getOscoreId(subject);
}
switch (res) {
case TokenRepository.OK :
this.d.deliverRequest(ex);
return;
case TokenRepository.UNAUTHZ :
failUnauthz(ex);
EndpointContext ctx = request.getSourceContext();
if (ctx != null && ctx instanceof DtlsEndpointContext) {
// The canAccess() method of the TokenRepository has returned Unauthorized, even though
// the request from the Client was protected. That can happen only if the Resource Server
// has deleted the previously stored access token associated with the Client.
//
// Consequently, the secure association with the Client must be terminated.
// The handling below terminates a DTLS connection. In case of an OSCORE association,
// the corresponding OSCORE Security Context has been already deleted in the removeToken()
// method of the TokenRepository, following the deletion of the access token.
InetSocketAddress peerAddress = (InetSocketAddress) ex.getPeersIdentity();
Connector connector = coapEndpoint.getConnector();
if (connector instanceof DTLSConnector) {
((DTLSConnector) connector).close(peerAddress);
}
}
return;
case TokenRepository.FORBID :
r = new Response(ResponseCode.FORBIDDEN);
try {
r.setPayload(this.asRCH.getHints(ex.getCurrentRequest(),
kid).EncodeToBytes());
r.getOptions().setContentFormat(Constants.APPLICATION_ACE_CBOR);
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
LOGGER.severe("cnonce creation failed: " + e.getMessage());
ex.sendResponse(r); //Send response without payload
}
ex.sendResponse(r);
return;
case TokenRepository.METHODNA :
r = new Response(ResponseCode.METHOD_NOT_ALLOWED);
try {
r.setPayload(this.asRCH.getHints(ex.getCurrentRequest(),
kid).EncodeToBytes());
r.getOptions().setContentFormat(Constants.APPLICATION_ACE_CBOR);
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
LOGGER.severe("cnonce creation failed: " + e.getMessage());
ex.sendResponse(r);
}
ex.sendResponse(r);
return;
default :
LOGGER.severe("Error during scope evaluation,"
+ " unknown result: " + res);
ex.sendResponse(new Response(
ResponseCode.INTERNAL_SERVER_ERROR));
return;
}
} catch (AceException e) {
LOGGER.severe("Error in CoapDeliverer.deliverRequest(): "
+ e.getMessage());
} catch (IntrospectionException e) {
LOGGER.info("Introspection error, "
+ "message processing aborted: " + e.getMessage());
if (e.getMessage().isEmpty()) {
ex.sendResponse(new Response(
ResponseCode.INTERNAL_SERVER_ERROR));
}
CBORObject map = CBORObject.NewMap();
map.Add(Constants.ERROR, Constants.INVALID_REQUEST);
map.Add(Constants.ERROR_DESCRIPTION, e.getMessage());
r = new Response(ResponseCode.BAD_REQUEST);
r.setPayload(map.EncodeToBytes());
ex.sendResponse(r);
}
}
/**
* Fail a request with 4.01 Unauthorized.
*
*/
private void failUnauthz(Exchange ex) {
Response r = new Response(ResponseCode.UNAUTHORIZED);
try {
r.setPayload(this.asRCH.getHints(
ex.getCurrentRequest(), null).EncodeToBytes());
r.getOptions().setContentFormat(Constants.APPLICATION_ACE_CBOR);
ex.sendResponse(r);
} catch (InvalidKeyException | NoSuchAlgorithmException
| AceException e) {
LOGGER.severe("cnonce creation failed: " + e.getMessage());
ex.sendResponse(r); //Just send UNAUTHORIZED without a payload
}
}
@Override
public void deliverResponse(Exchange exchange, Response response) {
this.d.deliverResponse(exchange, response);
}
public byte[] GetBytes(String str)
{
char[] chars = str.toCharArray();
byte[] bytes = new byte[chars.length * 2];
for (int i = 0; i < chars.length; i++)
{
bytes[i * 2] = (byte) (chars[i] >> 8);
bytes[i * 2 + 1] = (byte) chars[i];
}
return bytes;
}
}