GroupOSCORESubResourceNodenameCred.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.oscore.rs.oscoreGroupManager;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.californium.core.CoapResource;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.coap.Request;
import org.eclipse.californium.core.coap.Response;
import org.eclipse.californium.core.server.resources.CoapExchange;
import com.upokecenter.cbor.CBORObject;
import com.upokecenter.cbor.CBORType;
import org.eclipse.californium.cose.AlgorithmID;
import org.eclipse.californium.cose.CoseException;
import org.eclipse.californium.cose.KeyKeys;
import org.eclipse.californium.cose.OneKey;
import se.sics.ace.AceException;
import se.sics.ace.Constants;
import se.sics.ace.GroupcommErrors;
import se.sics.ace.GroupcommParameters;
import se.sics.ace.Util;
import se.sics.ace.coap.CoapReq;
import se.sics.ace.oscore.GroupInfo;
import se.sics.ace.rs.TokenRepository;
/**
* Definition of the Group OSCORE group-membership sub-resource /nodes/NODENAME/cred
* for the group members with node name "NODENAME"
*/
public class GroupOSCORESubResourceNodenameCred extends CoapResource {
private Map<String, GroupInfo> existingGroupInfo = new HashMap<>();
/**
* Constructor
* @param resId the resource identifier
* @param existingGroupInfo the set of information of the existing OSCORE groups
*/
public GroupOSCORESubResourceNodenameCred(String resId, Map<String, GroupInfo> existingGroupInfo) {
// set resource identifier
super(resId);
// set display name
getAttributes().setTitle("Group OSCORE Group-Membership Sub-Resource \"nodes/NODENAME/cred\" " + resId);
this.existingGroupInfo = existingGroupInfo;
}
@Override
public void handlePOST(CoapExchange exchange) {
System.out.println("POST request reached the GM");
// Retrieve the entry for the target group, using the last path segment of
// the URI path as the name of the OSCORE group
GroupInfo targetedGroup = existingGroupInfo.get(this.getParent().getParent().getParent().getName());
// This should never happen if existing groups are maintained properly
if (targetedGroup == null) {
exchange.respond(CoAP.ResponseCode.SERVICE_UNAVAILABLE,
"Error when retrieving material for the OSCORE group");
return;
}
if (targetedGroup.getStatus() == false) {
// The group is currently not active
CBORObject responseMap = CBORObject.NewMap();
CBORObject aceGroupcommError = CBORObject.NewMap();
aceGroupcommError.Add(0, GroupcommErrors.GROUP_NOT_ACTIVE);
responseMap.Add(Constants.PROBLEM_DETAIL_ACE_GROUPCOMM_ERROR, aceGroupcommError);
responseMap.Add(Constants.PROBLEM_DETAIL_KEY_TITLE, GroupcommErrors.DESCRIPTION[GroupcommErrors.GROUP_NOT_ACTIVE]);
byte[] responsePayload = responseMap.EncodeToBytes();
exchange.respond(CoAP.ResponseCode.SERVICE_UNAVAILABLE,
responsePayload,
Constants.APPLICATION_CONCISE_PROBLEM_DETAILS_CBOR);
return;
}
String groupName = targetedGroup.getGroupName();
// This should never happen if active groups are maintained properly
if (!groupName.equals(this.getParent().getParent().getParent().getName())) {
exchange.respond(CoAP.ResponseCode.SERVICE_UNAVAILABLE,
"Error when retrieving material for the OSCORE group");
return;
}
String subject = null;
Request request = exchange.advanced().getCurrentRequest();
try {
subject = CoapReq.getInstance(request).getSenderId();
} catch (AceException e) {
System.err.println("Error while retrieving the client identity: " + e.getMessage());
}
if (subject == null) {
// At this point, this should not really happen,
// due to the earlier check at the Token Repository
exchange.respond(CoAP.ResponseCode.UNAUTHORIZED,
"Unauthenticated client tried to get access");
return;
}
if (!targetedGroup.isGroupMember(subject)) {
// The requester is not a current group member.
CBORObject responseMap = CBORObject.NewMap();
CBORObject aceGroupcommError = CBORObject.NewMap();
aceGroupcommError.Add(0, GroupcommErrors.ONLY_FOR_GROUP_MEMBERS);
responseMap.Add(Constants.PROBLEM_DETAIL_ACE_GROUPCOMM_ERROR, aceGroupcommError);
responseMap.Add(Constants.PROBLEM_DETAIL_KEY_TITLE, GroupcommErrors.DESCRIPTION[GroupcommErrors.ONLY_FOR_GROUP_MEMBERS]);
byte[] responsePayload = responseMap.EncodeToBytes();
exchange.respond(CoAP.ResponseCode.FORBIDDEN,
responsePayload,
Constants.APPLICATION_CONCISE_PROBLEM_DETAILS_CBOR);
return;
}
if (!(targetedGroup.getGroupMemberName(subject)).equals(this.getParent().getName())) {
// The requester is not the group member associated to this sub-resource.
exchange.respond(CoAP.ResponseCode.FORBIDDEN,
"Operation permitted only to the group member associated to this sub-resource");
return;
}
if (targetedGroup.getGroupMemberRoles((targetedGroup.getGroupMemberName(subject))) ==
(1 << GroupcommParameters.GROUP_OSCORE_MONITOR)) {
// The requester is a monitor, hence it is not supposed to have a Sender ID.
CBORObject responseMap = CBORObject.NewMap();
CBORObject aceGroupcommError = CBORObject.NewMap();
aceGroupcommError.Add(0, GroupcommErrors.INCONSISTENCY_WITH_ROLES);
responseMap.Add(Constants.PROBLEM_DETAIL_ACE_GROUPCOMM_ERROR, aceGroupcommError);
responseMap.Add(Constants.PROBLEM_DETAIL_KEY_TITLE, GroupcommErrors.DESCRIPTION[GroupcommErrors.INCONSISTENCY_WITH_ROLES]);
byte[] responsePayload = responseMap.EncodeToBytes();
exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
responsePayload,
Constants.APPLICATION_CONCISE_PROBLEM_DETAILS_CBOR);
return;
}
byte[] requestPayload = exchange.getRequestPayload();
if(requestPayload == null) {
exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
"A payload must be present");
return;
}
CBORObject AuthCredUpdateRequest = CBORObject.DecodeFromBytes(requestPayload);
// The payload of the Authentication Credential Update Request must be a CBOR Map
if (!AuthCredUpdateRequest.getType().equals(CBORType.Map)) {
exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
"The payload must be a CBOR map");
return;
}
if (!AuthCredUpdateRequest.ContainsKey(GroupcommParameters.CLIENT_CRED)) {
exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
"Missing parameter: 'client_cred'");
return;
}
if (!AuthCredUpdateRequest.ContainsKey(GroupcommParameters.CNONCE)) {
exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
"Missing parameter: 'cnonce'");
return;
}
if (!AuthCredUpdateRequest.ContainsKey(GroupcommParameters.CLIENT_CRED_VERIFY)) {
exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
"Missing parameter: 'client_cred_verify'");
return;
}
// Retrieve 'client_cred'
CBORObject clientCred = AuthCredUpdateRequest.get(CBORObject.FromObject(GroupcommParameters.CLIENT_CRED));
// client_cred cannot be Null
if (clientCred == null) {
exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
"The parameter 'client_cred' cannot be Null");
return;
}
OneKey publicKey = null;
boolean valid = false;
if (clientCred.getType() != CBORType.ByteString) {
exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
"The parameter 'client_cred' must be a CBOR byte string");
return;
}
byte[] clientCredBytes = clientCred.GetByteString();
switch(targetedGroup.getAuthCredFormat()) {
case Constants.COSE_HEADER_PARAM_KCCS:
CBORObject ccs = CBORObject.DecodeFromBytes(clientCredBytes);
if (ccs.getType() == CBORType.Map) {
// Retrieve the public key from the CCS
publicKey = Util.ccsToOneKey(ccs);
valid = true;
}
else {
exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
"Invalid format of authentication credential");
return;
}
break;
case Constants.COSE_HEADER_PARAM_KCWT:
CBORObject cwt = CBORObject.DecodeFromBytes(clientCredBytes);
if (cwt.getType() == CBORType.Array) {
// Retrieve the public key from the CWT
// TODO
}
else {
exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
"Invalid format of authentication credential");
return;
}
break;
case Constants.COSE_HEADER_PARAM_X5CHAIN:
// Retrieve the public key from the certificate
if (clientCred.getType() == CBORType.ByteString) {
// TODO
}
else {
exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
"Invalid format of authentication credential");
return;
}
break;
default:
exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
"Invalid format of authentication credential");
return;
}
if (publicKey == null || valid == false) {
exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
"Invalid public key format");
return;
}
// Sanity check on the type of public key
if (targetedGroup.getSignAlg().equals(AlgorithmID.ECDSA_256) ||
targetedGroup.getSignAlg().equals(AlgorithmID.ECDSA_384) ||
targetedGroup.getSignAlg().equals(AlgorithmID.ECDSA_512)) {
// Invalid public key format
if (!publicKey.get(KeyKeys.KeyType).
equals(targetedGroup.getSignParams().get(0).get(0)) || // alg capability: key type
!publicKey.get(KeyKeys.KeyType).
equals(targetedGroup.getSignParams().get(1).get(0)) || // key capability: key type
!publicKey.get(KeyKeys.EC2_Curve).
equals(targetedGroup.getSignParams().get(1).get(1))) // key capability: curve
{
CBORObject errorResponseMap = CBORObject.NewMap();
CBORObject aceGroupcommError = CBORObject.NewMap();
aceGroupcommError.Add(0, GroupcommErrors.INCOMPATIBLE_CRED);
errorResponseMap.Add(Constants.PROBLEM_DETAIL_ACE_GROUPCOMM_ERROR, aceGroupcommError);
errorResponseMap.Add(Constants.PROBLEM_DETAIL_KEY_TITLE, GroupcommErrors.DESCRIPTION[GroupcommErrors.INCOMPATIBLE_CRED]);
byte[] errorResponsePayload = errorResponseMap.EncodeToBytes();
exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
errorResponsePayload,
Constants.APPLICATION_CONCISE_PROBLEM_DETAILS_CBOR);
return;
}
}
if (targetedGroup.getSignAlg().equals(AlgorithmID.EDDSA)) {
// Invalid public key format
if (!publicKey.get(KeyKeys.KeyType).
equals(targetedGroup.getSignParams().get(0).get(0)) || // alg capability: key type
!publicKey.get(KeyKeys.KeyType).
equals(targetedGroup.getSignParams().get(1).get(0)) || // key capability: key type
!publicKey.get(KeyKeys.OKP_Curve).
equals(targetedGroup.getSignParams().get(1).get(1))) // key capability: curve
{
CBORObject errorResponseMap = CBORObject.NewMap();
CBORObject aceGroupcommError = CBORObject.NewMap();
aceGroupcommError.Add(0, GroupcommErrors.INCOMPATIBLE_CRED);
errorResponseMap.Add(Constants.PROBLEM_DETAIL_ACE_GROUPCOMM_ERROR, aceGroupcommError);
errorResponseMap.Add(Constants.PROBLEM_DETAIL_KEY_TITLE, GroupcommErrors.DESCRIPTION[GroupcommErrors.INCOMPATIBLE_CRED]);
byte[] errorResponsePayload = errorResponseMap.EncodeToBytes();
exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
errorResponsePayload,
Constants.APPLICATION_CONCISE_PROBLEM_DETAILS_CBOR);
return;
}
}
// Retrieve the proof-of-possession nonce from the Client
CBORObject cnonce = AuthCredUpdateRequest.get(CBORObject.FromObject(GroupcommParameters.CNONCE));
// A client nonce must be included for proof-of-possession
if (cnonce == null) {
exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
"The parameter 'cnonce' cannot be Null");
return;
}
// The client nonce must be wrapped in a binary string
if (!cnonce.getType().equals(CBORType.ByteString)) {
exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
"The parameter 'cnonce' must be a CBOR byte string");
return;
}
// Check the PoP evidence over (scope | rsnonce | cnonce), using the Client's public key
CBORObject clientPopEvidence = AuthCredUpdateRequest.get(CBORObject.FromObject(GroupcommParameters.CLIENT_CRED_VERIFY));
// A client PoP evidence must be included
if (clientPopEvidence == null) {
exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
"The parameter 'client_cred_verify' cannot be Null");
return;
}
// The client PoP evidence must be wrapped in a binary string for joining OSCORE groups
if (!clientPopEvidence.getType().equals(CBORType.ByteString)) {
exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
"The parameter 'client_cred_verify' must be a CBOR byte string");
return;
}
byte[] rawClientPopEvidence = clientPopEvidence.GetByteString();
PublicKey pubKey = null;
try {
pubKey = publicKey.AsPublicKey();
} catch (CoseException e) {
System.out.println(e.getMessage());
exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR,
"Failed to use the Client's public key to verify the PoP evidence");
return;
}
if (pubKey == null) {
exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR,
"Failed to use the Client's public key to verify the PoP evidence");
return;
}
// Rebuild the original 'scope' from the Join Request
CBORObject cborArrayScope = CBORObject.NewArray();
int myRoles = targetedGroup.getGroupMemberRoles((targetedGroup.getGroupMemberName(subject)));
cborArrayScope.Add(groupName);
cborArrayScope.Add(myRoles);
byte[] scope = cborArrayScope.EncodeToBytes();
// Retrieve the original 'rsnonce' specified in the Token POST response
String rsNonceString = TokenRepository.getInstance().getRsnonce(subject);
if(rsNonceString == null) {
// Return an error response, with a new nonce for PoP of the Client's private key
CBORObject responseMap = CBORObject.NewMap();
byte[] rsnonce = new byte[8];
new SecureRandom().nextBytes(rsnonce);
responseMap.Add(GroupcommParameters.KDCCHALLENGE, rsnonce);
TokenRepository.getInstance().setRsnonce(subject, Base64.getEncoder().encodeToString(rsnonce));
byte[] responsePayload = responseMap.EncodeToBytes();
exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
responsePayload,
Constants.APPLICATION_ACE_GROUPCOMM_CBOR);
return;
}
byte[] rsnonce = Base64.getDecoder().decode(rsNonceString);
int offset = 0;
byte[] serializedScopeCBOR = CBORObject.FromObject(scope).EncodeToBytes();
byte[] serializedGMNonceCBOR = CBORObject.FromObject(rsnonce).EncodeToBytes();
byte[] serializedCNonceCBOR = cnonce.EncodeToBytes();
byte[] popInput = new byte [serializedScopeCBOR.length +
serializedGMNonceCBOR.length +
serializedCNonceCBOR.length];
System.arraycopy(serializedScopeCBOR, 0, popInput, offset, serializedScopeCBOR.length);
offset += serializedScopeCBOR.length;
System.arraycopy(serializedGMNonceCBOR, 0, popInput, offset, serializedGMNonceCBOR.length);
offset += serializedGMNonceCBOR.length;
System.arraycopy(serializedCNonceCBOR, 0, popInput, offset, serializedCNonceCBOR.length);
// The group mode is used. The PoP evidence is a signature
if (targetedGroup.getMode() != GroupcommParameters.GROUP_OSCORE_PAIRWISE_MODE_ONLY) {
int signKeyCurve = 0;
if (publicKey.get(KeyKeys.KeyType).equals(org.eclipse.californium.cose.KeyKeys.KeyType_EC2))
signKeyCurve = publicKey.get(KeyKeys.EC2_Curve).AsInt32();
else if (publicKey.get(KeyKeys.KeyType).equals(org.eclipse.californium.cose.KeyKeys.KeyType_OKP))
signKeyCurve = publicKey.get(KeyKeys.OKP_Curve).AsInt32();
// This should never happen, due to the previous sanity checks
if (signKeyCurve == 0) {
exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR,
"Error when setting up the signature verification");
return;
}
// Invalid Client's PoP signature
if (!Util.verifySignature(signKeyCurve, pubKey, popInput, rawClientPopEvidence)) {
CBORObject errorResponseMap = CBORObject.NewMap();
CBORObject aceGroupcommError = CBORObject.NewMap();
aceGroupcommError.Add(0, GroupcommErrors.INVALID_POP_EVIDENCE);
errorResponseMap.Add(Constants.PROBLEM_DETAIL_ACE_GROUPCOMM_ERROR, aceGroupcommError);
errorResponseMap.Add(Constants.PROBLEM_DETAIL_KEY_TITLE, GroupcommErrors.DESCRIPTION[GroupcommErrors.INVALID_POP_EVIDENCE]);
byte[] errorResponsePayload = errorResponseMap.EncodeToBytes();
exchange.respond(CoAP.ResponseCode.BAD_REQUEST,
errorResponsePayload,
Constants.APPLICATION_CONCISE_PROBLEM_DETAILS_CBOR);
return;
}
}
// Only the pairwise mode is used. The PoP evidence is a MAC
else {
// TODO
}
byte[] senderId = targetedGroup.getGroupMemberSenderId(subject).GetByteString();
if (!targetedGroup.storeAuthCred(senderId, clientCred)) {
exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR,
"Error when storing the authentication credential");
return;
}
// Respond to the Authentication Credential Update Request
Response coapResponse = new Response(CoAP.ResponseCode.CHANGED);
exchange.respond(coapResponse);
}
}