GroupOSCOREJoinValidator.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.oscore.rs;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import com.upokecenter.cbor.CBORObject;
import com.upokecenter.cbor.CBORType;
import se.sics.ace.AceException;
import se.sics.ace.Constants;
import se.sics.ace.Util;
import se.sics.ace.rs.AudienceValidator;
import se.sics.ace.rs.ScopeValidator;
/**
* Audience and scope validator for testing purposes.
* This validator expects the scopes to be either Strings as in OAuth 2.0,
* or Byte Arrays to join OSCORE groups as per draft-ietf-ace-key-groupcomm-oscore
*
* The actions are expected to be integers corresponding to the
* values for RESTful actions in <code>Constants</code>.
*
* @author Marco Tiloca
*
*/
public class GroupOSCOREJoinValidator implements AudienceValidator, ScopeValidator {
/**
* The audiences we recognize
*/
private Set<String> myAudiences;
/**
* The audiences acting as OSCORE Group Managers
* Each of these audiences is also included in the main set "myAudiences"
*/
private Set<String> myGMAudiences;
/**
* The group-membership resources exported by the OSCORE Group Manager to access an OSCORE group.
*
* Each entry of the list contains the full path to a group-membership resource and the last
* path segment is the name of the associated OSCORE group, e.g. ace-group/GROUP_NAME
*/
private Set<String> myJoinResources;
private String rootGroupMembershipResource;
/**
* Maps the scopes to a map that maps the scope's resources to the actions
* allowed on that resource
*/
private Map<String, Map<String, Set<Short>>> myScopes;
/**
* Constructor.
*
* @param myAudiences the audiences that this validator should accept
* @param myScopes the scopes that this validator should accept
* @param rootGroupMemberResource the path of the root Group Membership Resource, i.e., "ace-group"
*/
public GroupOSCOREJoinValidator(Set<String> myAudiences,
Map<String, Map<String, Set<Short>>> myScopes,
String rootGroupMemberResource) {
this.myAudiences = new HashSet<>();
this.myGMAudiences = new HashSet<>();
this.myJoinResources = new HashSet<>();
this.myScopes = new HashMap<>();
if (myAudiences != null) {
this.myAudiences.addAll(myAudiences);
} else {
this.myAudiences = Collections.emptySet();
}
if (myScopes != null) {
this.myScopes.putAll(myScopes);
} else {
this.myScopes = Collections.emptyMap();
}
this.rootGroupMembershipResource = rootGroupMemberResource;
}
/**
* Get a string including the common URI path to all group-membership
* resources, i.e. the full URI path minus the group name
*
* @return the common URI path to all group-membership resources
*/
public String getRootGroupMembershipResource() {
return this.rootGroupMembershipResource;
}
/**
* Get the list of audiences acting as OSCORE Group Managers.
*
* @return the audiences that this validator considers as OSCORE Group Managers
*/
public synchronized Set<String> getAllGMAudiences() {
if (this.myGMAudiences != null) {
return this.myGMAudiences;
}
return Collections.emptySet();
}
/**
* Set the list of audiences acting as OSCORE Group Managers.
* Check that each of those audiences are in the main set "myAudiences".
*
* @param myGMAudiences the audiences that this validator considers as OSCORE Group Managers
*
* @throws AceException if the group manager is not an accepted audience
*/
public synchronized void setGMAudiences(Set<String> myGMAudiences) throws AceException {
if (myGMAudiences != null) {
for (String foo : myGMAudiences) {
if (!this.myAudiences.contains(foo)) {
throw new AceException("This OSCORE Group Manager is not an accepted audience");
}
this.myGMAudiences.add(foo);
}
} else {
this.myGMAudiences = Collections.emptySet();
}
}
/**
* Remove an audience acting as OSCORE Group Manager from "myGMAudiences".
* This method does not remove the audience from the main set "myAudiences".
*
* @param GMAudience the audience acting as OSCORE Group Manager to be removed
*
* @return true if the specified audience was included and has been removed, false otherwise.
*/
public synchronized boolean removeGMAudience(String GMAudience){
if (GMAudience != null)
return this.myGMAudiences.remove(GMAudience);
return false;
}
/**
* Remove all the audiences acting as OSCORE Group Manager from "myGMAudiences".
* This method does not remove the audiences from the main set "myAudiences".
*
*/
public synchronized void removeAllGMAudiences(){
this.myGMAudiences.clear();
}
/**
* Get the list of group-membership resources to access an OSCORE group.
*
* Each entry of the list contains the full path to a group-membership resource, and the last
* path segment is the name of the associated OSCORE group, e.g. ace-group/GROUP_NAME
*
* @return the resources that this validator considers as group-membership resources to access an OSCORE group
*/
public synchronized Set<String> getAllJoinResources() {
if (this.myJoinResources != null) {
return this.myJoinResources;
}
return Collections.emptySet();
}
/**
* Set the list of group-membership resources to access an OSCORE group.
*
* Each entry of the list contains the full path to a group-membership resource, and the last
* path segment is the name of the associated OSCORE group, e.g. ace-group/GROUP_NAME
*
* @param myJoinResources the resources that this validator considers as group-membership resources to access an OSCORE group
* .
* @throws AceException FIXME: when thrown?
*/
public synchronized void setJoinResources(Set<String> myJoinResources) throws AceException {
if (myJoinResources != null) {
for (String foo : myJoinResources)
this.myJoinResources.add(foo);
} else {
this.myJoinResources = Collections.emptySet();
}
}
/**
* Remove a group-membership resource to access an OSCORE group from "myJoinResources".
*
* The group-membership resource to remove is specified by its full path, where the last
* path segment is the name of the associated OSCORE group, e.g. ace-group/GROUP_NAME
*
* @param joinResource the group-membership resource to remove.
*
* @return true if the specified resource was included and has been removed, false otherwise.
*/
public synchronized boolean removeJoinResource(String joinResource){
if (joinResource != null)
return this.myJoinResources.remove(joinResource);
return false;
}
/**
* Remove all the group-membership resources to access an OSCORE group from "myJoinResources".
*
*/
public synchronized void removeAllJoinResources(){
this.myJoinResources.clear();
}
@Override
public boolean match(String aud) {
return this.myAudiences.contains(aud);
}
@Override
public boolean scopeMatch(CBORObject scope, String resourceId, Object actionId)
throws AceException {
if (!scope.getType().equals(CBORType.TextString) && !scope.getType().equals(CBORType.ByteString)) {
throw new AceException("Scope must be a Text String or a Byte String");
}
String scopeStr;
boolean isJoinResource = false;
boolean scopeMustBeBinary = false;
if (this.myJoinResources.contains(resourceId))
isJoinResource = true;
scopeMustBeBinary = isJoinResource;
if (scope.getType().equals(CBORType.TextString)) {
if (scopeMustBeBinary)
return false;
String[] scopes = scope.AsString().split(" ");
for (String subscope : scopes) {
Map<String, Set<Short>> resources = this.myScopes.get(subscope);
if (resources == null) {
continue;
}
if (resources.containsKey(resourceId)) {
if (resources.get(resourceId).contains(actionId)) {
return true;
}
}
}
return false;
}
else if (scope.getType().equals(CBORType.ByteString) && isJoinResource) {
byte[] rawScope = scope.GetByteString();
CBORObject cborScope = CBORObject.DecodeFromBytes(rawScope);
if (!cborScope.getType().equals(CBORType.Array)) {
throw new AceException("Invalid scope format for joining OSCORE groups");
}
for (int entryIndex = 0; entryIndex < cborScope.size(); entryIndex++) {
CBORObject scopeEntry = cborScope.get(entryIndex);
if (scopeEntry.size() != 2)
throw new AceException("Scope must have two elements, i.e. Group ID and list of roles");
// Retrieve the Group ID of the OSCORE group
CBORObject scopeElement = scopeEntry.get(0);
if (scopeElement.getType().equals(CBORType.TextString)) {
scopeStr = scopeElement.AsString();
}
else {throw new AceException("The Group Name must be a CBOR Text String");}
// Retrieve the role or list of roles
scopeElement = scopeEntry.get(1);
if (scopeElement.getType().equals(CBORType.Integer)) {
int roleSet = scopeElement.AsInt32();
if (roleSet <= 0)
throw new AceException("The roles must be encoded as a CBOR Unsigned Integer greater than 0");
Set<Integer> roleIdSet = Util.getGroupOSCORERoles(roleSet);
for (Integer elem : roleIdSet) {
if (elem.intValue() < Constants.GROUP_OSCORE_ROLES.length)
continue;
else {
throw new AceException("Unrecognized role");
}
}
}
else {throw new AceException("Invalid format of roles");}
Map<String, Set<Short>> resources = this.myScopes.get(rootGroupMembershipResource + "/" + scopeStr);
// resourceId is the name of the OSCORE group
if (resources != null && resources.containsKey(resourceId)) {
if (resources.get(resourceId).contains(actionId)) {
return true;
}
}
}
return false;
}
// This includes the case where the scope is encoded as a CBOR Byte String,
// but the targeted resource is not a group-membership resource to access an OSCORE group.
// In fact, no processing for byte string scopes are defined, other than
// the one implemented above according to draft-ietf-ace-key-groupcomm-oscore
else if (scope.getType().equals(CBORType.ByteString))
throw new AceException("Unknown processing for this byte string scope");
return false;
}
@Override
public boolean scopeMatchResource(CBORObject scope, String resourceId)
throws AceException {
if (!scope.getType().equals(CBORType.TextString) && !scope.getType().equals(CBORType.ByteString)) {
throw new AceException("Scope must be a Text String or a Byte String");
}
String scopeStr;
boolean isJoinResource = false;
boolean scopeMustBeBinary = false;
if (this.myJoinResources.contains(resourceId))
isJoinResource = true;
scopeMustBeBinary = isJoinResource;
if (scope.getType().equals(CBORType.TextString)) {
if (scopeMustBeBinary)
return false;
String[] scopes = scope.AsString().split(" ");
for (String subscope : scopes) {
Map<String, Set<Short>> resources = this.myScopes.get(subscope);
if (resources == null) {
continue;
}
if (resources.containsKey(resourceId)) {
return true;
}
}
return false;
}
else if (scope.getType().equals(CBORType.ByteString) && isJoinResource) {
byte[] rawScope = scope.GetByteString();
CBORObject cborScope = CBORObject.DecodeFromBytes(rawScope);
if (!cborScope.getType().equals(CBORType.Array)) {
throw new AceException("Invalid scope format for joining OSCORE groups");
}
for (int entryIndex = 0; entryIndex < cborScope.size(); entryIndex++) {
CBORObject scopeEntry = cborScope.get(entryIndex);
if (scopeEntry.size() != 2)
throw new AceException("Scope must have two elements, i.e. Group ID and list of roles");
// Retrieve the group name of the OSCORE group
CBORObject scopeElement = scopeEntry.get(0);
if (scopeElement.getType().equals(CBORType.TextString)) {
scopeStr = scopeElement.AsString();
}
else {throw new AceException("The Group ID must be a CBOR Text String");}
// Retrieve the role or list of roles
scopeElement = scopeEntry.get(1);
if (scopeElement.getType().equals(CBORType.Integer)) {
int roleSet = scopeElement.AsInt32();
if (roleSet <= 0)
throw new AceException("The roles must be encoded as a CBOR Unsigned Integer greater than 0");
Set<Integer> roleIdSet = Util.getGroupOSCORERoles(roleSet);
for (Integer elem : roleIdSet) {
if (elem.intValue() < Constants.GROUP_OSCORE_ROLES.length)
continue;
else {
throw new AceException("Unrecognized role");
}
}
}
else {throw new AceException("Invalid format of roles");}
Map<String, Set<Short>> resources = this.myScopes.get(rootGroupMembershipResource + "/" + scopeStr);
// resourceId is the name of the OSCORE group
if (resources != null && resources.containsKey(resourceId))
return true;
}
return false;
}
// This includes the case where the scope is encoded as a CBOR Byte String,
// but the targeted resource is not a group-membership resource to access an OSCORE group.
// In fact, no processing for byte string scopes are defined, other than
// the one implemented above according to draft-ietf-ace-key-groupcomm-oscore
else if (scope.getType().equals(CBORType.ByteString))
throw new AceException("Unknown processing for this byte string scope");
return false;
}
@Override
public boolean isScopeMeaningful(CBORObject scope) throws AceException {
if (!scope.getType().equals(CBORType.TextString)) {
throw new AceException("Scope must be a String if no audience is specified");
}
return this.myScopes.containsKey(scope.AsString());
}
@Override
public boolean isScopeMeaningful(CBORObject scope, String aud) throws AceException {
if (!scope.getType().equals(CBORType.TextString) && !scope.getType().equals(CBORType.ByteString)) {
throw new AceException("Scope must be a Text String or a Byte String");
}
String scopeStr;
boolean scopeMustBeBinary = false;
boolean rsOSCOREGroupManager = false;
if (this.myGMAudiences.contains(aud)) {
rsOSCOREGroupManager = true;
}
scopeMustBeBinary = rsOSCOREGroupManager;
if (scope.getType().equals(CBORType.TextString)) {
if (scopeMustBeBinary)
return false;
return this.myScopes.containsKey(scope.AsString());
// The audiences are silently ignored
}
else if (scope.getType().equals(CBORType.ByteString) && rsOSCOREGroupManager) {
byte[] rawScope = scope.GetByteString();
CBORObject cborScope = CBORObject.DecodeFromBytes(rawScope);
if (!cborScope.getType().equals(CBORType.Array)) {
throw new AceException("Invalid scope format for joining OSCORE groups");
}
for (int entryIndex = 0; entryIndex < cborScope.size(); entryIndex++) {
CBORObject scopeEntry = cborScope.get(entryIndex);
if (!scopeEntry.getType().equals(CBORType.Array)) {
throw new AceException("Invalid scope format for joining OSCORE groups");
}
if (scopeEntry.size() != 2)
throw new AceException("A scope entry must have two elements, i.e. group name and list of roles");
// Retrieve the Group ID of the OSCORE group
CBORObject scopeElement = scopeEntry.get(0);
if (scopeElement.getType().equals(CBORType.TextString)) {
scopeStr = scopeElement.AsString();
}
else {throw new AceException("The group name must be a CBOR Text String");}
// Retrieve the role or list of roles
scopeElement = scopeEntry.get(1);
if (scopeElement.getType().equals(CBORType.Integer)) {
int roleSet = scopeElement.AsInt32();
if (roleSet <= 0)
throw new AceException("The roles must be encoded as a CBOR Unsigned Integer greater than 0");
Set<Integer> roleIdSet = Util.getGroupOSCORERoles(roleSet);
for (Integer elem : roleIdSet) {
if (elem.intValue() < Constants.GROUP_OSCORE_ROLES.length)
continue;
else {
throw new AceException("Unrecognized role");
}
}
}
else {throw new AceException("Invalid format of roles");}
if (this.myScopes.containsKey(rootGroupMembershipResource + "/" + scopeStr) == false)
return false;
}
return true;
}
// This includes the case where the scope is encoded as a CBOR Byte String,
// but the audience is not related to an OSCORE Group Manager.
// In fact, no processing for byte string scopes are defined, other than
// the one implemented above according to draft-ietf-ace-key-groupcomm-oscore
else if (scope.getType().equals(CBORType.ByteString))
throw new AceException("Unknown processing for this byte string scope");
return false;
}
@Override
public CBORObject getScope(String resource, short action) {
// TODO Auto-generated method stub
return null;
}
}