Permissioning Contracts: Simple Authorization Whitelist
This is an example of a basic permissioning contract that
maintains a whitelist of enodes allowed to participate in the
Enterprise Ethereum blockchain, and a list of administrative
accounts that can alter that whitelist.
In this model administrators are allowed to add and remove administrators, and
add and remove nodes. Clients can check whether a given connection
is permitted using the `connectionAllowed` function, which checks that the enode
addresses of both nodes involved in the connection are in the whitelist.
When a permission rule update occurs a `NodePermissionsUpdated` event is
emitted indicating that a rule change has occurred, and whether the rule change
makes the Enterprise Ethereum blockchain more permissive or adds new
restrictions.
When deploying the contract it is initialised with the account
deploying it as the first administrator. This account can then add
additional administrators and add nodes to the whitelist.
This contract serves as an example only. As written, it has shortcomings.
These need to be considered before using the code any production environment.
Some potentially desirable features that are not included in this example are:
- Protection against all administors being removed
- A list of current administrators
- The current whitelisted enodes
- Protection against a single administrator taking over the permissioning system
- A way to upgrade the contract logic
- Grouping or organization of whitelist members.
pragma solidity >=0.4.0 <0.6.0;
contract SimplePermissioning {
// Struct representing an enode
struct Enode {
bytes32 enodeHigh;
bytes32 enodeLow;
bytes16 enodeHost;
uint16 enodePort;
}
// Event emitted when a rules change occurs
event NodePermissionsUpdated(
bool addsRestrictions,
bool addsPermissions
);
// List of nodes permitted to participate in the network
mapping(bytes => Enode) private whitelist;
// List of accounts allowed to modify the network
mapping(address => bool) private adminList;
constructor() public {
// set the contract creator as the first admin
adminList[msg.sender] = true;
}
// Guard modifier for functions that can only be invokable by admins
modifier onlyAdmin() {
require(adminList[msg.sender] == true, "Cannot be called except by members of the admin list");
_;
}
// Add an admin to the contract
function addAdmin(address newAdmin) public onlyAdmin {
adminList[newAdmin] = true;
}
// Remove an admin from the contract
function removeAdmin(address oldAdmin) public onlyAdmin {
adminList[oldAdmin] = false;
}
// Check if a connection between two nodes will be permitted
function connectionAllowed(
bytes32 sourceEnodeHigh,
bytes32 sourceEnodeLow,
bytes16 sourceEnodeIp,
uint16 sourceEnodePort,
bytes32 destinationEnodeHigh,
bytes32 destinationEnodeLow,
bytes16 destinationEnodeIp,
uint16 destinationEnodePort
) public view returns (bytes32) {
// Check that both are in the whitelist
if (
enodeAllowed(sourceEnodeHigh, sourceEnodeLow, sourceEnodeIp, sourceEnodePort)
&& enodeAllowed(destinationEnodeHigh, destinationEnodeLow, destinationEnodeIp, destinationEnodePort)
) {
// If both are then indicate permitted by returning the first bit as set
return 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
} else {
// If one or neither are permitted then indicate not permitted by unsetting the first bit
return 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
}
}
// Check if a specified enode is in the whitelist
function enodeAllowed(
bytes32 sourceEnodeHigh,
bytes32 sourceEnodeLow,
bytes16 sourceEnodeIp,
uint16 sourceEnodePort
) public view returns (bool){
bytes memory key = computeKey(sourceEnodeHigh, sourceEnodeLow, sourceEnodeIp, sourceEnodePort);
Enode storage whitelistSource = whitelist[key];
if (whitelistSource.enodeHost > 0) {
return true;
}
}
// Add a specified enode to the whitelist
function addEnode(
bytes32 enodeHigh,
bytes32 enodeLow,
bytes16 enodeIp,
uint16 enodePort
) public onlyAdmin {
Enode memory newEnode = Enode(enodeHigh, enodeLow, enodeIp, enodePort);
bytes memory key = computeKey(enodeHigh, enodeLow, enodeIp, enodePort);
whitelist[key] = newEnode;
// Emit an event indicating a permissioning update occurred that only
// allows new nodes into the network
emit NodePermissionsUpdated(false, true);
}
// Remove a specified enode from the whitelist
function removeEnode(
bytes32 enodeHigh,
bytes32 enodeLow,
bytes16 enodeIp,
uint16 enodePort
) public onlyAdmin {
bytes memory key = computeKey(enodeHigh, enodeLow, enodeIp, enodePort);
Enode memory zeros = Enode(bytes32(0), bytes32(0), bytes16(0), 0);
whitelist[key] = zeros;
// Emit an event indicating a permissioning update occurred that can
// cause existing connections to now be disallowed
emit NodePermissionsUpdated(true, false);
}
// Compute a consistent hashkey for a given enode
function computeKey(
bytes32 enodeHigh,
bytes32 enodeLow,
bytes16 enodeIp,
uint16 enodePort
) public pure returns (bytes memory) {
return abi.encode(enodeHigh, enodeLow, enodeIp, enodePort);
}
}
Permissioning Contracts: memberGroups and authorizedUsers
This is an example permissioning model with four smart contract
interfaces for management, and methods for enforcement. These example interfaces
are for node permissioning.
A permissioned Enterprise Ethereum blockchain includes
smart contracts that implement the permissioning enforcement
interfaces specified in Section
,
along with whatever management functions are appropriate for the chosen
permissioning model.
The model in this example is based on the concept of a
network, that consists of a set of
memberGroups, each representing an enterprise
or organization, and each made up of authorizedUsers.
In this model nodes are added to or removed from the node list
of a memberGroup. If a memberGroup joins the network,
the nodes associated with that memberGroup are permitted to join.
Conversely, if the memberGroup is removed from the
network, the nodes associated with that memberGroup are
disconnected.
As noted, memberGroups are a collection of authorizedUsers. An
authorizedUser is represented by one or
more Ethereum accounts. This provides robustness in case a given user
loses the keys to one of their accounts, as well as reflecting the
reality that many users operate more than one account.
Each authorizedUser has individually specified permissions to act
on behalf of the memberGroup in administering the
Enterprise Ethereum blockchain. Depending on the specific
permissions they have, an authorizedUser can make changes, such as
adding a new authorizedUser to a memberGroup, adding a new
node to a memberGroup node list, or inviting another
memberGroup to join the Enterprise Ethereum blockchain.
A permissioning decider function is used to decide whether or not a
memberGroup is permitted to join the network, when to evict
a memberGroup, and especially whether to change the
permissioning decider itself.
This model uses four fundamental smart contract interfaces:
- `AuthorizedUser`
- `MemberGroup`
- `Network`
- `PermissioningDecider`.
AuthorizedUser
The `AuthorizedUser` smart contract contains authorizedUsers,
initialized with a name and an identifier, like an email address. Additional
information, such as alternative contact information or PGP public keys, could
also be included by implementations of `AuthorizedUser`.
pragma solidity ^0.5.1;
interface AuthorizedUser {
// Metadata
// Retrieve the user's name.
function getName() external view returns (string memory);
// Retrieve the user's identifier.
function getId() external view returns (string memory);
// Authorization mutation.
// Add an Ethereum account address for an authorizedUser. Multiple addresses
// can be added.
function addAddress(address _owner) external;
// Remove an Ethereum account address from an authorizedUser.
function removeAddress(address _owner) external;
// Authorization queries.
// Check if the authorizedUser owns a specific Ethereum account address.
function owns(address _owner) external view returns (bool);
// Network of trust (reputation) mutators.
// Check if the authorizedUser vouches for another (child) authorizedUser.
function hasEndorsed(AuthorizedUser _child) external view returns (bool);
// Vouch for another authorizedUser.
function endorse(AuthorizedUser _child) external;
// Stop vouching for an authorizedUser
function unendorse(AuthorizedUser _child) external;
// Network of trust backlinks. These are called by `endorse` and
// `unendorse` implementations respectively, to provide pointers about where
// to look for endorsements.
// Set the (parent) authorizedUser as vouching for the authorizedUser.
function recordEndorsement(AuthorizedUser _parent) external;
// Set the (parent) authorizedUser to no longer vouch for the authorizedUser.
function eraseEndorsement(AuthorizedUser _parent) external;
}
A user is responsible for deciding to endorse someone as an
authorizedUser. Some examples of how Alice might decide to endorse Bob
as an authorizedUser include:
- alice@a.net sends an email to bob@a.net, asking to confirm Bob's
authorizedUser address.
- alice@a.net sends an email to bob@b.com, asking Bob to join a video call to
assert his ownership of a given Ethereum account.
- Alice walks over to Bob's desk and asks for his Ethereum account
address.
Any caller can add any address as a parent of an authorizedUser. To
authenticate an authorizedUser, the contract follows the parent links
and checks that the corresponding child link is present.
MemberGroup
The `MemberGroup` smart contract represents a group of
authorizedUsers and their permissions.
pragma solidity ^0.5.1;
import "./AuthorizedUser.sol";
interface MemberGroup {
// Metadata
// Retrieve the memberGroup name.
function getName() external view returns (string memory);
// Retrieve the permissions for an authorizedUser.
function permission(AuthorizedUser) external view returns (uint);
// Retrieve the number of authorizedUsers in the memberGroup.
function memberCount() external view returns (uint);
// Retrieve an authorizedUser, specified by index idx, from the memberGroup.
function getMember(uint idx) external view returns (AuthorizedUser);
// Managing membership of the group
// Add an authorizedUser to the memberGroup. Requester needs
// `CAN_ADD_USER` permission.
function addUser(AuthorizedUser requester, AuthorizedUser object,
uint _permission) external;
// Remove a authorizedUsers from the memberGroup. Requester needs
// `CAN_REMOVE_USER` permission.
function removeUser(AuthorizedUser requester,
AuthorizedUser object) external;
// Events
// Emitted when an authorizedUser is added to a memberGroup.
event MemberAdded(AuthorizedUser _user, uint _permission);
// Emitted when an authorizedUser is removed from a memberGroup.
event MemberRemoved(AuthorizedUser _user, uint _permission);
}
An authorizedUser is given permissions as follows:
- The authorizedUser who calls the `addUser` function (the requester)
proposes a set of permissions as the `_permission` parameter.
- The contract does a bitwise AND of the requester's own permissions with
the request.
- The result is recorded as the permission for the newly added
authorizedUser.
A partial definition of permissions could be as follows:
pragma solidity ^0.5.1;
contract Permissions {
// Permission to add or remove an authorizedUser.
uint constant public CAN_ADD_USER = 0x1;
uint constant public CAN_REMOVE_USER = 0x2;
// Permission to change the node list of a memberGroup.
uint constant public CAN_ADD_NODE = 0x4;
uint constant public CAN_REMOVE_NODE = 0x8;
// Permission to vote for other memberGroups to join or leave the network.
uint constant public CAN_INVITE_MEMBERGROUP = 0x10;
uint constant public CAN_UNINVITE_MEMBERGROUP = 0x20;
// Permission to vote for a new permissioning decider
uint constant public CAN_PROPOSE_DECIDER = 0x100;
uint constant public ADMIN = 0x1ff;
function meets(uint have, uint needed) public pure returns (bool) {
return have & needed == needed;
}
}
Network
As described above, networks are a collection of memberGroups.
Each memberGroup manages a node list (a list of [[enode]]
URLs, corresponding to the nodes allowed to connect to the
Enterprise Ethereum blockchain that are controlled by the
memberGroup). Any authorizedUser that is part of the
memberGroup and has the `CAN_ADD_NODE` permission can add to the
node list for that memberGroup.
To add a memberGroup to, or remove a memberGroup from, the
Enterprise Ethereum blockchain, every memberGroup that is already
a member can vote to `invite` or `uninvite` that memberGroup. The
permissioning decider for the network determines whether to update
the network members, given the current set of requests.
A memberGroup can have `WRITE` or `READ` permissions, set as part
of adding it to the network. A node belonging to a
memberGroup that has `WRITE` permission can submit a transaction,
but transactions from a node in a memberGroup that only has
`READ` permission result in the `transactionAllowed` method returning
`res: false` for that transaction.
pragma solidity ^0.5.1;
import "./MemberGroup.sol";
import "./AuthorizedUser.sol";
import "./PermissioningDecider.sol";
interface Network {
// Node queries.
// Retrieve the memberGroup that a node is part of.
function memberGroupOf(string calldata _node) external view
returns (MemberGroup);
// Retrieve the number of nodes in the memberGroup.
function memberGroupsNodeCount(MemberGroup) external view
returns (uint);
// Retrieve a node, specified by index idx, from the memberGroup.
function memberGroupsNode(MemberGroup, uint idx) external view
returns (string memory);
// Authorization queries.
// Retrieve the permissions for the memberGroup.
function permission(MemberGroup) external view returns (uint);
// Check if 2 nodes are allowed to connect to one another
function connectionAllowed(
bytes32 sourceEnodeHigh,
bytes32 sourceEnodeLow,
bytes32 sourceIp,
bytes32 sourcePort,
bytes32 destinationEnodeHigh,
bytes32 destinationEnodeLow,
bytes32 destinationIp,
bytes32 destinationPort
) external view returns (bytes32);
// Group administration.
// Add a node to a memberGroup. This will fail unless the authorizedUser has
// `CAN_ADD_NODE` permission.
function addNode(MemberGroup, AuthorizedUser, string calldata _node) external;
// Remove a node from a memberGroup. This will fail unless the authorizedUser has
// `CAN_REMOVE_NODE` permission
function removeNode(MemberGroup, AuthorizedUser, string calldata _node) external;
// Group membership queries.
// Retrieve the number of memberGroups.
function memberGroupCount() external view returns (uint);
// Retrieve a memberGroup, specified by index idx
function getmemberGroup(uint idx) external view
returns (MemberGroup);
// Network vote counts.
// Retrieve the number of invites for the memberGroup to have `READ`
// permissions in the context of the network.
function readInvitesReceived(MemberGroup) external view
returns (uint);
// Retrieve the number of invites for the memberGroup to have `WRITE`
// permissions in the context of the network.
function writeInvitesReceived(MemberGroup) external view
returns (uint);
// Retrieve the number of uninvites for the memberGroup to leave the
// network.
function uninvitesReceived(MemberGroup) external view returns (uint);
// Group membership mutators.
// Invite a memberGroup to join the network. This will fail unless the authorizedUser has
// `CAN_INVITE_MEMBERGROUP` permission.
function invite(MemberGroup _invitee, MemberGroup _ginviter,
AuthorizedUser _uinviter, string calldata _node, uint _perm) external;
// Uninvite a memberGroup from the network. This will fail unless the authorizedUser has
// `CAN_UNINVITE_MEMBERGROUP` permission.
function uninvite(MemberGroup _invitee, MemberGroup _ginviter,
AuthorizedUser _uinviter) external;
// Rule inspection.
// Retrieve the permissioning decider function currently in use.
function decider() external view returns (PermissioningDecider);
// Rule vote counts.
// Retrieve the number of votes received for the permissioning decider.
function deciderVotesReceived(PermissioningDecider) external view
returns (uint);
// Retrieve the permissioning decider nominated by the memberGroup.
// Useful for admin weighting.
function nominatedDecider(MemberGroup) external view
returns (PermissioningDecider);
// Change the rule engine.
// Propose a new permissioning decider.
function proposeDecider(PermissioningDecider _next,
MemberGroup _gproposer, AuthorizedUser _uproposer)
external;
// Emitted events.
// The permissions set was updated
event NodePermissionsUpdated(bool addsRestrictions, bool addsPermissions);
// A node was added to a memberGroup.
event NodeAdded(MemberGroup _changed_group, string _node);
// A node was removed from a memberGroup.
event NodeRemoved(MemberGroup _changed_group, string _node);
// An authorizedUser has invited a memberGroup to join the network.
event memberGroupInvited(MemberGroup _invited_group,
uint _permission);
// An authorizedUser has uninvited a memberGroup from the network.
event memberGroupUnInvited(MemberGroup _uninvited_group,
uint _permission);
// A memberGroup was added to the network.
event memberGroupAdded(MemberGroup _added_group,
uint _permission);
// A memberGroup was removed from the network.
event memberGroupRemoved(MemberGroup _removed_group,
uint _permission);
// A permissioning decider function was swapped to a new one.
event DeciderSwapped(PermissioningDecider _old, PermissioningDecider _new);
}
The `Network` contract checks whether the caller has permission to call
the `invite`, `uninvite`, `proposeDecider`, `addNode`, and `removeNode`
functions. The granularity of permissions is implementation-dependent.
PermissioningDecider
The `PermissioningDeciders` smart contract< customizes the bylaws of a
`Network` smart contract.
pragma solidity ^0.5.1;
import "./MemberGroup.sol";
import "./Network.sol";
interface PermissioningDecider {
// The permission the memberGroup now has, if approved.
function inviteApproved(Network, MemberGroup) external view
returns (uint8);
// Whether the network will remove the memberGroup.
function inviteRevoked(Network, MemberGroup) external view
returns (bool);
// Whether the network will change its permissioning decider.
function swapDecider(Network, PermissioningDecider) external view
returns (bool);
}
Some example `PermissioningDeciders` include:
- Static: memberGroups are never removed or added from the `Network`. Any
attempt to change the `PermissioningDecider` will fail.
- AutoApprove: memberGroups are automatically included (or removed) when
invited (or uninvited). The decider swaps the first time it is asked.
- AdminRun: The `Network` has an administrator group, which is the only vote
counted for approving or revoking approval of a memberGroup, or changing
the `PermissionDecider`.
- MajorityRules: A prospective memberGroup needs more than half of the
current memberGroups to invite it for membership. A prospective
`PermissioningDecider` needs more than half of the current groups to nominate it
before this `Decider` relinquishes control.
Node Blacklisting
Blacklisting a node from a memberGroup level can be done by by
adding the following functions to the `MemberGroup` smart contract.
interface MemberGroup {
...
// Blacklist an authorizedUser.
function blacklistNode(AuthorizedUser, string _node) interface;
// Remove an authorizedUser from the blacklist.
function unblacklistNode(AuthorizedUser, string _node) interface;
...
}
Blacklisting of nodes for the whole
Enterprise Ethereum blockchain can be done by adding the following
functions to the `Network` and `PermissioningDecider` smart contracts.
interface Network {
...
// Vote to add an authorizedUser to the blacklist.
voteToBlacklist(MemberGroup, AuthorizedUser, string _node) external;
// Vote to remove an authorizedUser from the blacklist.
voteToUnblacklist(MemberGroup, AuthorizedUser, string _node) external;
// Retrieve the number of votes for the node to be added to the blacklist.
blacklistVotesReceived(string _node) external view returns (uint);
// Retrieve the number of votes for the node to be removed from the
// blacklist.
unblacklistVotesReceived(string _node) external view returns (uint);
// Emitted when a node is added to the blacklist.
event NodeBlacklisted(MemberGroup _blacklisted_group, string _node);
// Emitted when a node is removed from the blacklist.
event NodeUnblacklisted(MemberGroup _unblacklisted_group, string _node);
...
}
interface PermissioningDecider {
...
// Whether the node will be added to the blacklist.
function blacklistApproved(Network, string _node) external view
returns (bool);
// Returns whether the node will be removed from the blacklist.
function unblacklistApproved(Network, string _node) external view
returns (bool);
...
}
Legal Notice
The copyright in this document is owned by Enterprise Ethereum Alliance Inc.
(“EEA” or “Enterprise Ethereum Alliance”).
No modifications, edits or changes to the information in this document are permitted.
Subject to the terms and conditions described herein, this document may be duplicated for internal use,
provided that all copies contain all proprietary notices and disclaimers included herein.
Except as otherwise provided herein, no license, express or implied,
by estoppel or otherwise, to any intellectual property rights are granted herein.
Use of this document and any related intellectual property incorporated herein,
is also governed by the Bylaws, Intellectual Property Rights Policy
and other governing documents and policies of EEA
and is subject to the disclaimers and limitations described below.
No use or display of any of the following names or marks "Enterprise Ethereum Alliance",
the acronym "EEA", the EEA logo, or any combination thereof,
to claim compliance with or conformance to this document (or similar statements)
is permitted absent EEA membership and express written permission from the EEA.
The EEA is in process of developing a compliance testing and certification program
only for the EEA members in good standing, which it targets to launch towards the end of 2020.
THE CONTENTS OF THIS DOCUMENT ARE PROVIDED "AS IS" WITH NO WARRANTIES WHATSOEVER,
INCLUDING ANY WARRANTY OF MERCHANTABILITY, NONINFRINGEMENT, FITNESS FOR ANY PARTICULAR PURPOSE,
SATISFACTORY QUALITY, OR REASONABLE SKILL OR CARE, OR ANY WARRANTY ARISING OUT OF ANY COURSE OF DEALING,
USAGE, TRADE PRACTICE, PROPOSAL, SPECIFICATION OR SAMPLE.
EEA DOES NOT WARRANT THAT THIS DOCUMENT IS COMPLETE OR WITHOUT ERROR
AND DISCLAIMS ANY WARRANTIES TO THE CONTRARY.
Each user of this document hereby acknowledges that sofftware or products
implementing the technology specified in this document ("EEA-Compliant Products")
may be subject to various regulatory controls under the laws and regulations
of various governments worldwide.
Such laws and regulatory controls may govern, among other things, the combination,
operation, use, implementation and distribution of EEA-Compliant Products.
Examples of such laws and regulatory controls include, but are not limited to,
airline regulatory controls, telecommunications regulations,
finance industry and security regulations, technology transfer controls,
health and safety and other types of regulations.
Each user of this document is solely responsible for the compliance by their
EEA-Compliant Products with any such laws and regulations and for obtaining
any and all required authorizations, permits, or licenses for their EEA-Compliant Products
related to such regulations within the applicable jurisdictions.
Each user of this document acknowledges that nothing in this document or the relevant specification
provides any information or assistance in connection with securing such compliance,
authorizations or licenses.
NOTHING IN THIS DOCUMENT CREATES ANY WARRANTIES WHATSOEVER REGARDING
THE APPLICABILITY OR NON-APPLICABILITY OF ANY SUCH LAWS OR REGULATIONS
OR THE SUITABILITY OR NON-SUITABILITY OF ANY SUCH PRODUCT OR SERVICE FOR USE IN ANY JURISDICTION.
EEA has not investigated or made an independent determination regarding title or non-infringement
of any technologies that may be incorporated, described or referenced in this document.
Use of this document or implementation of any technologies described or referenced herein
may therefore infringe undisclosed third-party patent rights or other intellectual property rights.
The user is solely responsible for making all assessments relating to title and non-infringement
of any technology, standard, or specification referenced in this document
and for obtaining appropriate authorization to use such technologies, standards, and specifications,
including through the payment of any required license fees.
NOTHING IN THIS DOCUMENT CREATES ANY WARRANTIES OF TITLE OR NONINFRINGEMENT
WITH RESPECT TO ANY TECHNOLOGIES, STANDARDS OR SPECIFICATIONS REFERENCED OR INCORPORATED
INTO THIS DOCUMENT.
IN NO EVENT SHALL EEA OR ANY OF ITS MEMBERS BE LIABLE TO THE USER OR TO A THIRD PARTY
FOR ANY CLAIM ARISING FROM OR RELATING TO THE USE OF THIS DOCUMENT, INCLUDING, WITHOUT LIMITATION,
A CLAIM THAT SUCH USE INFRINGES A THIRD PARTY’S INTELLECTUAL PROPERTY RIGHTS
OR THAT IT FAILS TO COMPLY WITH APPLICABLE LAWS OR REGULATIONS. BY USE OF THIS DOCUMENT,
THE USER WAIVES ANY SUCH CLAIM AGAINST EEA AND ITS MEMBERS RELATING TO THE USE OF THIS DOCUMENT.
EEA reserves the right to adopt any changes or alterations to this document
as it deems necessary or appropriate without any notice.
User is solely responsible for determining whether this document has been superseded
by a later version or a different document.