EdhocLayer.java
/*******************************************************************************
* Copyright (c) 2020 RISE and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* and Eclipse Distribution License v1.0 which accompany this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v20.html
* and the Eclipse Distribution License is available at
* http://www.eclipse.org/org/documents/edl-v10.html.
*
* Contributors:
* Marco Tiloca (RISE)
* Rikard Höglund (RISE)
*
******************************************************************************/
package org.eclipse.californium.edhoc;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.upokecenter.cbor.CBORException;
import com.upokecenter.cbor.CBORObject;
import com.upokecenter.cbor.CBORType;
import java.io.ByteArrayInputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.HashMap;
import java.util.Set;
import org.eclipse.californium.core.coap.EmptyMessage;
import org.eclipse.californium.core.coap.Request;
import org.eclipse.californium.core.coap.Response;
import org.eclipse.californium.core.coap.CoAP.ResponseCode;
import org.eclipse.californium.core.network.Exchange;
import org.eclipse.californium.core.network.stack.AbstractLayer;
import org.eclipse.californium.cose.AlgorithmID;
import org.eclipse.californium.cose.OneKey;
import org.eclipse.californium.oscore.OSCoreCtx;
import org.eclipse.californium.oscore.OSCoreCtxDB;
import org.eclipse.californium.oscore.OSException;
/**
*
* Applies EDHOC mechanics at stack layer.
*
*/
public class EdhocLayer extends AbstractLayer {
private static final boolean debugPrint = true;
/**
* The logger
*/
private static final Logger LOGGER = LoggerFactory.getLogger(EdhocLayer.class);
/**
* The OSCORE context database
*/
OSCoreCtxDB ctxDb;
/**
* Map of existing EDHOC sessions
*/
HashMap<CBORObject, EdhocSession> edhocSessions;
/**
* Map of the EDHOC peer public keys
*/
HashMap<CBORObject, OneKey> peerPublicKeys;
/**
* Map of the EDHOC peer credentials
*/
HashMap<CBORObject, CBORObject> peerCredentials;
/**
* Set of used EDHOC Connection IDs
*/
Set<CBORObject> usedConnectionIds;
// Lookup identifier to be associated with the OSCORE Security Context
private final String uriLocal = "coap://localhost";
// The size of the Replay Window to use in an OSCORE Recipient Context
private int OSCORE_REPLAY_WINDOW;
// The size to consider for MAX_UNFRAGMENTED SIZE
private int MAX_UNFRAGMENTED_SIZE;
/**
* Build the EdhocLayer
*
* @param ctxDb OSCORE context database
* @param edhocSessions map of current EDHOC sessions
* @param peerPublicKeys map containing the EDHOC peer public keys
* @param peerCredentials map containing the EDHOC peer credentials
* @param usedConnectionIds set containing the used EDHOC connection IDs
* @param OSCORE_REPLAY_WINDOW size of the Replay Window to use in an OSCORE Recipient Context
* @param MAX_UNFRAGMENTED_SIZE size of MAX_UNFRAGMENTED_SIZE to use in an OSCORE Security Context
*/
public EdhocLayer(OSCoreCtxDB ctxDb,
HashMap<CBORObject, EdhocSession> edhocSessions,
HashMap<CBORObject, OneKey> peerPublicKeys,
HashMap<CBORObject, CBORObject> peerCredentials,
Set<CBORObject> usedConnectionIds,
int OSCORE_REPLAY_WINDOW,
int MAX_UNFRAGMENTED_SIZE) {
this.ctxDb = ctxDb;
this.edhocSessions = edhocSessions;
this.peerPublicKeys = peerPublicKeys;
this.peerCredentials = peerCredentials;
this.usedConnectionIds = usedConnectionIds;
this.OSCORE_REPLAY_WINDOW = OSCORE_REPLAY_WINDOW;
this.MAX_UNFRAGMENTED_SIZE = MAX_UNFRAGMENTED_SIZE;
LOGGER.warn("Initializing EDHOC layer");
}
@Override
public void sendRequest(final Exchange exchange, final Request request) {
LOGGER.warn("Sending request through EDHOC layer");
if (request.getOptions().hasOscore() && request.getOptions().hasEdhoc()) {
LOGGER.warn("Combined EDHOC+OSCORE request");
// Retrieve the Security Context used to protect the request
OSCoreCtx ctx = getContextForOutgoing(exchange);
// The connection identifier of this peer is its Recipient ID
byte[] recipientId = ctx.getRecipientId();
CBORObject connectionIdentifierInitiatorCbor = CBORObject.FromObject(recipientId);
// Retrieve the EDHOC session associated to C_R and storing EDHOC message_3
EdhocSession session = this.edhocSessions.get(connectionIdentifierInitiatorCbor);
// Consistency checks
if (session == null) {
System.err.println("Unable to retrieve the EDHOC session when sending an EDHOC+OSCORE request\n");
return;
}
byte[] connectionIdentifierInitiator = session.getConnectionId();
if (!session.isInitiator() ||
session.getCurrentStep() != Constants.EDHOC_SENT_M3 ||
!Arrays.equals(recipientId, connectionIdentifierInitiator)) {
System.err.println("Retrieved inconsistent EDHOC session when sending an EDHOC+OSCORE request");
return;
}
// Extract EDHOC message_3, from the stored CBOR sequence (C_R, EDHOC message_3)
byte[] storedSequence = session.getMessage3();
CBORObject[] sequenceElements = CBORObject.DecodeSequenceFromBytes(storedSequence);
byte[] edhocMessage3 = sequenceElements[1].EncodeToBytes();
// Original OSCORE payload from the request
byte[] oldOscorePayload = request.getPayload();
if (debugPrint) {
Util.nicePrint("EDHOC+OSCORE: EDHOC message_3", edhocMessage3);
Util.nicePrint("EDHOC+OSCORE: Old OSCORE payload", oldOscorePayload);
}
// Build the new OSCORE payload, as composed of two concatenated elements
// 1. A CBOR data item, i.e., EDHOC message_3 (of type Byte String)
// 2. The original OSCORE payload
int newOscorePayloadLength = edhocMessage3.length + oldOscorePayload.length;
// Abort if the payload of the EDHOC+OSCORE request exceeds MAX_UNFRAGMENTED_SIZE
int maxUnfragmentedSize = ctx.getMaxUnfragmentedSize();
if (newOscorePayloadLength > maxUnfragmentedSize) {
throw new IllegalStateException("The payload of the EDHOC+OSCORE request is exceeding MAX_UNFRAGMENTED_SIZE");
}
byte[] newOscorePayload = new byte[newOscorePayloadLength];
System.arraycopy(edhocMessage3, 0, newOscorePayload, 0, edhocMessage3.length);
System.arraycopy(oldOscorePayload, 0, newOscorePayload, edhocMessage3.length, oldOscorePayload.length);
if (debugPrint) {
Util.nicePrint("EDHOC+OSCORE: New OSCORE payload", newOscorePayload);
}
// Set the new OSCORE payload as payload of the EDHOC+OSCORE request
request.setPayload(newOscorePayload);
}
super.sendRequest(exchange, request);
}
@Override
public void sendResponse(Exchange exchange, Response response) {
LOGGER.warn("Sending response through EDHOC layer");
super.sendResponse(exchange, response);
}
@Override
public void receiveRequest(Exchange exchange, Request request) {
LOGGER.warn("Receiving request through EDHOC layer");
if (request.getOptions().hasEdhoc()) {
if (!request.getOptions().hasOscore()) {
String responseString = new String("Received a request including the EDHOC option but" +
" not including the OSCORE option\n");
System.err.println(responseString);
sendErrorResponse(exchange, responseString, ResponseCode.BAD_REQUEST);
return;
}
if (request.getPayload() == null) {
String responseString = new String("Received a request including the EDHOC option but" +
" not including a payload\n");
System.err.println(responseString);
sendErrorResponse(exchange, responseString, ResponseCode.BAD_REQUEST);
return;
}
LOGGER.warn("Combined EDHOC+OSCORE request");
boolean error = false;
// Retrieve the received payload combining EDHOC message_3 and the real OSCORE payload
byte[] oldPayload = request.getPayload();
if (debugPrint) {
Util.nicePrint("EDHOC+OSCORE: received payload", oldPayload);
}
CBORObject edhocMessage3 = null;
ByteArrayInputStream myStream = null;
myStream = new ByteArrayInputStream(oldPayload);
try {
edhocMessage3 = CBORObject.Read(myStream);
}
catch (CBORException e) {
System.err.println("CBORException: " + e.getMessage());
error = true;
}
catch (NullPointerException e) {
System.err.println("NullPointerException: " + e.getMessage());
error = true;
}
if (edhocMessage3 == null || edhocMessage3.getType() != CBORType.ByteString) {
error = true;
}
int oscoreCiphertextLen = oldPayload.length - edhocMessage3.EncodeToBytes().length;
byte[] newPayload = new byte[oscoreCiphertextLen];
int readBytes = -1;
try {
readBytes = myStream.read(newPayload, 0, oscoreCiphertextLen);
}
catch (NullPointerException e) {
System.err.println("NullPointerException: " + e.getMessage());
error = true;
}
catch (IndexOutOfBoundsException e) {
System.err.println("IndexOutOfBoundsException: " + e.getMessage());
error = true;
}
if (readBytes != oscoreCiphertextLen) {
error = true;
}
// The EDHOC+OSCORE request is malformed
if (error == true) {
String responseString = new String("Invalid EDHOC+OSCORE request");
System.err.println(responseString);
sendErrorResponse(exchange, responseString, ResponseCode.BAD_REQUEST);
return;
}
// Prepare the actual OSCORE request, by replacing the payload
request.setPayload(newPayload);
if (debugPrint) {
Util.nicePrint("EDHOC+OSCORE: OSCORE request payload", newPayload);
}
// Rebuild the CBOR sequence (C_R, EDHOC message_3)
List<CBORObject> edhocObjectList = new ArrayList<>();
// Add C_R, by encoding the 'kid' from the OSCORE option
byte[] kid = getKid(request.getOptions().getOscore());
CBORObject cR = MessageProcessor.encodeIdentifier(kid);
edhocObjectList.add(cR);
// Add EDHOC message_3, i.e., the CBOR data item retrieved from the received message
edhocObjectList.add(edhocMessage3);
byte[] mySequence = Util.buildCBORSequence(edhocObjectList);
if (debugPrint) {
Util.nicePrint("EDHOC+OSCORE: rebuilt CBOR sequence (C_R, EDHOC message_3)", mySequence);
}
CBORObject kidCbor = CBORObject.FromObject(kid);
EdhocSession mySession = edhocSessions.get(kidCbor);
// Consistency checks
if (mySession == null) {
String responseString = new String("Unable to retrieve the EDHOC session when"
+ " receiving an EDHOC+OSCORE request\n");
System.err.println(responseString);
sendErrorResponse(exchange, responseString, ResponseCode.BAD_REQUEST);
return;
}
byte[] connectionIdentifierInitiator = mySession.getPeerConnectionId();
byte[] connectionIdentifierResponder = mySession.getConnectionId();
if (mySession.isInitiator() ||
mySession.getCurrentStep() != Constants.EDHOC_SENT_M2 ||
!Arrays.equals(kid, connectionIdentifierResponder)) {
System.err.println("Retrieved inconsistent EDHOC session when receiving an EDHOC+OSCORE request");
return;
}
// This EDHOC resource does not support the use of the EDHOC+OSCORE request
if (mySession.getApplicationProfile().getSupportCombinedRequest() == false) {
System.err.println("This EDHOC resource does not support the use of the EDHOC+OSCORE request\n");
Util.purgeSession(mySession, connectionIdentifierResponder, edhocSessions, usedConnectionIds);
String errMsg = new String("This EDHOC resource does not support the use of the EDHOC+OSCORE request");
byte[] nextMessage = MessageProcessor.writeErrorMessage(Constants.ERR_CODE_UNSPECIFIED_ERROR,
Constants.EDHOC_MESSAGE_3,
false, connectionIdentifierInitiator,
errMsg, null);
ResponseCode responseCode = ResponseCode.BAD_REQUEST;
sendErrorMessage(exchange, nextMessage, responseCode);
return;
}
// The combined request cannot be used if the Responder has to send message_4
if (mySession.getApplicationProfile().getUseMessage4() == true) {
System.err.println("Cannot receive the combined EDHOC+OSCORE request if message_4 is expected\n");
Util.purgeSession(mySession, connectionIdentifierResponder, edhocSessions, usedConnectionIds);
String errMsg = new String("Cannot receive the combined EDHOC+OSCORE request if message_4 is expected");
byte[] nextMessage = MessageProcessor.writeErrorMessage(Constants.ERR_CODE_UNSPECIFIED_ERROR,
Constants.EDHOC_MESSAGE_3,
false, connectionIdentifierInitiator,
errMsg, null);
ResponseCode responseCode = ResponseCode.BAD_REQUEST;
sendErrorMessage(exchange, nextMessage, responseCode);
return;
}
// Process EDHOC message_3
List<CBORObject> processingResult = new ArrayList<CBORObject>();
byte[] nextMessage = new byte[] {};
processingResult = MessageProcessor.readMessage3(mySequence, true, null, edhocSessions, peerPublicKeys,
peerCredentials, usedConnectionIds);
if (processingResult.get(0) == null || processingResult.get(0).getType() != CBORType.ByteString) {
String responseString = new String("Internal error when processing EDHOC Message 3");
System.err.println(responseString);
sendErrorResponse(exchange, responseString, ResponseCode.INTERNAL_SERVER_ERROR);
return;
}
// A non-zero length response payload would be an EDHOC Error Message
nextMessage = processingResult.get(0).GetByteString();
// The protocol has successfully completed
if (nextMessage.length == 0) {
cR = processingResult.get(1);
mySession = edhocSessions.get(cR);
if (mySession == null) {
System.err.println("Inconsistent state before sending EDHOC Message 3");
String responseString = new String("Inconsistent state before sending EDHOC Message 3");
sendErrorResponse(exchange, responseString, ResponseCode.INTERNAL_SERVER_ERROR);
return;
}
if (mySession.getCurrentStep() != Constants.EDHOC_AFTER_M3) {
System.err.println("Inconsistent state after sending EDHOC Message 3");
Util.purgeSession(mySession, connectionIdentifierResponder, edhocSessions, usedConnectionIds);
String responseString = new String("Inconsistent state before sending EDHOC Message 3");
sendErrorResponse(exchange, responseString, ResponseCode.BAD_REQUEST);
return;
}
/* Invoke the EDHOC-Exporter to produce OSCORE input material */
byte[] masterSecret = EdhocSession.getMasterSecretOSCORE(mySession);
byte[] masterSalt = EdhocSession.getMasterSaltOSCORE(mySession);
if (debugPrint) {
Util.nicePrint("OSCORE Master Secret", masterSecret);
Util.nicePrint("OSCORE Master Salt", masterSalt);
}
/* Setup the OSCORE Security Context */
// The Sender ID of this peer is the EDHOC connection identifier of the other peer
byte[] senderId = connectionIdentifierInitiator;
// The Recipient ID of this peer is the EDHOC connection identifier of this peer
byte[] recipientId = connectionIdentifierResponder;
int selectedCipherSuite = mySession.getSelectedCipherSuite();
AlgorithmID alg = EdhocSession.getAppAEAD(selectedCipherSuite);
AlgorithmID hkdf = EdhocSession.getAppHkdf(selectedCipherSuite);
OSCoreCtx ctx = null;
try {
ctx = new OSCoreCtx(masterSecret, false, alg, senderId,
recipientId, hkdf, OSCORE_REPLAY_WINDOW, masterSalt, null, MAX_UNFRAGMENTED_SIZE);
} catch (OSException e) {
Util.purgeSession(mySession, connectionIdentifierResponder, edhocSessions, usedConnectionIds);
String responseString = new String("Error when deriving the OSCORE Security Context");
System.err.println(responseString + " " + e.getMessage());
sendErrorResponse(exchange, responseString, ResponseCode.INTERNAL_SERVER_ERROR);
return;
}
try {
ctxDb.addContext(uriLocal, ctx);
} catch (OSException e) {
Util.purgeSession(mySession, connectionIdentifierResponder, edhocSessions, usedConnectionIds);
String responseString = new String("Error when adding the OSCORE Security Context to the context database");
System.err.println(responseString + " " + e.getMessage());
sendErrorResponse(exchange, responseString, ResponseCode.INTERNAL_SERVER_ERROR);
return;
}
// Remove the EDHOC option
request.getOptions().setEdhoc(false);
// The next step is to pass the OSCORE request to the next layer for processing
}
// An EDHOC error message has to be returned in response to EDHOC message_3
// The session has been possibly purged while attempting to process message_3
else {
int responseCodeValue = processingResult.get(1).AsInt32();
ResponseCode responseCode = ResponseCode.valueOf(responseCodeValue);
sendErrorMessage(exchange, nextMessage, responseCode);
return;
}
}
super.receiveRequest(exchange, request);
}
@Override
public void receiveResponse(Exchange exchange, Response response) {
LOGGER.warn("Receiving response through EDHOC layer");
super.receiveResponse(exchange, response);
}
@Override
public void sendEmptyMessage(Exchange exchange, EmptyMessage message) {
super.sendEmptyMessage(exchange, message);
}
@Override
public void receiveEmptyMessage(Exchange exchange, EmptyMessage message) {
super.receiveEmptyMessage(exchange, message);
}
/**
* Returns the OSCORE Context that was used to protect this outgoing
* exchange (outgoing request or response).
*
* @param e the exchange
* @return the OSCORE Context used to protect the exchange (if any)
*/
private OSCoreCtx getContextForOutgoing(Exchange e) {
String uri = e.getRequest().getURI();
if (uri == null) {
return null;
} else {
try {
return ctxDb.getContext(uri);
} catch (OSException exception) {
System.err.println("Error when retrieving the OSCORE Security Context " + exception.getMessage());
return null;
}
}
}
/**
* Retrieve KID value from an OSCORE option.
*
* @param oscoreOption the OSCORE option
* @return the KID value
*/
static byte[] getKid(byte[] oscoreOption) {
if (oscoreOption.length == 0) {
return null;
}
// Parse the flag byte
byte flagByte = oscoreOption[0];
int n = flagByte & 0x07;
int k = flagByte & 0x08;
int h = flagByte & 0x10;
byte[] kid = null;
int index = 1;
// Partial IV
index += n;
// KID Context
if (h != 0) {
int s = oscoreOption[index];
index += s + 1;
}
// KID
if (k != 0) {
kid = Arrays.copyOfRange(oscoreOption, index, oscoreOption.length);
}
return kid;
}
/*
* Send a CoAP error message in response to the received EDHOC+OSCORE request
*/
private void sendErrorResponse(Exchange exchange, String message, ResponseCode code) {
byte[] errorMessage = new byte[] {};
errorMessage = message.getBytes(Constants.charset);
Response errorResponse = new Response(code);
errorResponse.setPayload(errorMessage);
exchange.sendResponse(errorResponse);
}
/*
* Send an EDHOC Error Message in response to the received EDHOC+OSCORE request
*/
private void sendErrorMessage(Exchange exchange, byte[] nextMessage, ResponseCode responseCode) {
if (!MessageProcessor.isErrorMessage(nextMessage, false)) {
System.err.println("Inconsistent state before sending EDHOC Error Message");
String responseString = new String("Inconsistent state before sending EDHOC Error Message");
sendErrorResponse(exchange, responseString, ResponseCode.INTERNAL_SERVER_ERROR);
return;
}
Response myResponse = new Response(responseCode);
myResponse.getOptions().setContentFormat(Constants.APPLICATION_EDHOC_CBOR_SEQ);
myResponse.setPayload(nextMessage);
exchange.sendResponse(myResponse);
return;
}
}