Last updated: 24 April 1997.
The initial approach to implementing a specified security policy in an SSErl node is described. The policy is specified in a module of prescribed form, which defines the various constraints for the node, and especially the security monitor function which is called from the apply BIF to vet all external function calls. Following this is a discussion of some of the new features of SSErl, and a survey of issues under active debate.
k_apply
glue routine.
The k_apply
SSErl glue routine is shown below:
k_apply(From,M,F,A) when atom(M) -> % call security monitor (if present) to vet this call Chk = case get_dict(apply_chk) of {AM,AF} when atom(AM), atom(AF), AM == M -> ok; {AM,AF} when atom(AM), atom(AF) -> apply(AM,AF,[From,M,F,A]); _ -> ok end, % now alias module name and perform actual apply Mod = alias_module(M), apply(Mod,F,A);
A key feature of this approach is the use of Erlang's pattern matching
ability to resolve which of many possible function clauses to invoke.
This permits, I believe, a clean and relatively easy to validate
specification of the desired policy related to which calls are permissable.
The function has the form check(From,M,F,A)
where
From
is the module originating the function call,
M
is the module called, F
is the function
called with arguments A
. The monitor function can
match clauses on the module name or function name, or perform further
tests on the arguments supplied, as desired.
For example, the monitor function could look as follows:
check(M,M,_,_) -> ok; check(_,erlang,_,_) -> ok; check(_,_,module_info,_) -> ok; check(_,safety,_,_) -> ok; check(_,file,read_file,[F]) -> valid_name(F), ok; ... % many other clauses removed check(_,test,_,_) -> ok; check(From,M,F,A) -> exit({policy_violation,{apply,M,F,A}}).This indicates that intra-module calls are ok; as are any calls to functions in the erlang, safety, and test modules; calls to module_info in any module; and calls to file:read_file(F) provided F is a valid name. All other calls will result in a policy_violation exception.
In SSErl, this function may be specified when a new node is created, along with other aspects of the node (such as its alias table). Once specified, the function may not be changed for safety.
An example of a policy module is given below. It is intended for use by the SSErl test module. nb. this is an excerpt.
-module(safety_pol). % required policy functions we export -export([max_nrights/0, max_prights/0, aliases/0, init_servers/0, names/0, check/4]). %% max_nrights is the max list of rights for node capabilities %% subtract [delete_module,load_module,net_kernel,newnode] %% from full list of node rights %% the actual rights established may be further restricted by the parent node max_nrights() -> subtract(nrights(), list_to_set([delete_module,load_module,net_kernel,newnode])). %% max_prights is the max list of rights for process capabilities %% subtract [group_leader,open_port,priority] from full list of rights %% the actual rights established may be further restricted by the parent node max_prights() -> subtract(prights(),list_to_set([group_leader,open_port,priority])). %% aliases is the list of aliases to be used %% here alias the various stdlib modules compiled for SSErl env aliases() -> [{file,safe_file},{unix,safe_unix}, {gen,safe_gen},{gen_server,safe_gen_server},{proc_lib,safe_proc_lib}, {lists,safe_lists},{ordsets,safe_ordsets}, {random,safe_random},{string,safe_string} ]. %% init_servers calls any server init routines servicing new node, %% these servers are run in current node context init_servers() -> % start the safe versions of daemons used by safenode modules catch safe_file:start(), % assume quiet exit if already running ok. %% names returns the list of registered names to inherit, %% usually belonging to servers started by init_servers names() -> [safe_file_server]. %% check(From,M,F,A) - apply check security monitor, called on all applys %% returns ok | {EXIT,{policy_violation,{apply,M,F,A}} %% NOTE - this function MUST NOT call any external routines!!! check(M,M,_,_) -> ok; % intra module calls are ok check(_,erlang,_,_) -> ok; % erlang module calls are ok check(_,_,module_info,_) -> ok; % internal calls ok check(_,_,module_lambdas,_) -> ok; check(_,_,apply_lambda,_) -> ok; check(_,_,record_index,_) -> ok; check(_,sserl,_,_) -> ok; % SSErl modules are ok check(_,sserl_bif,_,_) -> ok; check(_,safety,_,_) -> ok; check(_,ssdebug,_,_) -> ok; check(_,file,get_cwd,_) -> ok; % check for acceptable file fns check(_,file,delete,_) -> ok; check(_,file,rename,_) -> ok; check(_,file,read_file,[F]) -> valid_name(F), ok; check(_,file,write_file,[F,_]) -> valid_name(F), ok; check(_,file,F,A) -> exit({policy_violation,{apply,file,F,A}}); check(_,safe_file,_,_) -> ok; % safe aliased variant ok check(_,unix,_,_) -> ok; % safe aliased variant ok check(_,safe_unix,_,_) -> ok; check(_,lists,_,_) -> ok; % modified standard libs are ok check(_,ordsets,_,_) -> ok; check(_,random,_,_) -> ok; check(_,string,_,_) -> ok; check(_,gen,_,_) -> ok; check(_,gen_server,_,_) -> ok; check(_,proc_lib,_,_) -> ok; check(_,safe_gen,_,_) -> ok; check(_,safe_gen_server,_,_) -> ok; check(_,safe_proc_lib,_,_) -> ok; check(_,c,_,_) -> ok; % unmod std libs assumed ok check(_,dict,_,_) -> ok; check(_,io,_,_) -> ok; check(_,io_lib,_,_) -> ok; check(_,io_lib_format,_,_) -> ok; check(_,io_lib_fread,_,_) -> ok; check(_,io_lib_pretty,_,_) -> ok; check(_,math,_,_) -> ok; check(_,queue,_,_) -> ok; check(_,regexp,_,_) -> ok; check(_,rpc,_,_) -> ok; check(_,shell,_,_) -> ok; check(_,shell_default,_,_) -> ok; check(_,timer,_,_) -> ok; check(_,test,_,_) -> ok; % allow modules in desired applications check(_,fun_test,_,_) -> ok; check(From,M,F,A) -> % everything else is rejected exit({policy_violation,{apply,M,F,A}}).
safety:policynode(NodeName,PolicyModule)
has been created. Its current implementation is given below.
%% policynode/2 - creates a subnode which implements the named policy module policynode(Name,Policy) -> CParent = get_dict(node), % get parent capability % establish various parameters from policy module NR = apply(Policy,max_nrights,[]), % node rights PR = apply(Policy,max_prights,[]), % process rights Ali = apply(Policy,aliases,[]), % alias table apply(Policy,init_servers,[]), % init servers Nam = lookup_names(apply(Policy,names,[]),[]), % registered names list Apply_chk = {Policy,check}, % security monitor function % create new node with specified policy newnode(CParent,Name,{NR,PR,Nam,Ali,nil,Apply_chk}).
The safety:policynode(NodeName,PolicyModule)
function may be called as follows to create a custom subnode:
PolNode = safety:policynode(testnode,my_policy_mod).Once it has been created, processes in it may be spawned as usual, eg.
spawn(PolNode,test,test,[]).
So with this approach, a SSErl node with a custom policy may be created by writing an appropriate policy module, and then simply using policynode to instantiate it.
There are some concerns about the impact on execution efficiency caused by all the checking of calls inline. This is certainly true at present, though there are some options available for future versions, such as:
Another concern is that the check function can become very large, as is already obvious from the early examples, with only a fraction of the standard library modules being listed. This, I believe, is indicative of need for support of a module hierarchy which can group many modules together. This could be used in the check function to accept all modules in a group with a single check clause. Some random thoughts on naming - one could follow the usual dotted convention (a la Java or Ada), but perhaps using a tuple for module names, with each element being a level would be nicer in Erlang. Would certainly make matching in the check function easier.
There are also some backdoor commands to the node manager, not implemented as BIFs, but available via custom messages. These are useful for debugging, but with serious safety concerns:
In the safety
library module, there are a number of utility
functions to assist in using the SSErl prototype.
A list of these is supplied by safety:help()
.
Of these, of special note are:
Ther is a serious issue concerning the interpretation of the rights associated with capabilities. Currently in both SSErl and the SafeErlang prototypes, they have two distinct uses:
I perceive some problems over the handling of rights of node capabilities (eg creator has right to shutdown node, but not any children in node, which means the node's knowledge of its capability perhaps should not include this right). This is probably going to end up being related to the question above on the meaning of rights.
There is the issue of whether the properties of a node (eg rights, alias table, apply check function) should be completely specified at the time of its creation, or whether they can be changed by a suitably privileged process whilst executing (eg standard pattern of spawning a wrapper which imposes some restrictions then applies some desired function). I prefer the former, but ...
There is a question over the meaning of pattern matching capabilities, esp in receive, as to whether you match on identical capabilities, or match different capabilities which refer to the same underlying object. It's raised due to a standard usage of PIDs in Erlang, matching a message with its response. eg. client process may send to a server (using some capability they know), then wait for a reply which includes the same pid (capability); whilst the server currently replies with self (which may not be identically the same capability), and indeed the server may have no way of knowing which capability a client used. There's agreement that the correct solution involves the use of references to mark transactions, but its a question of possibly supporting existing code.
The implementation of pattern matching and of the same guard is currently hindered by the need to query the parent nodes of the capabilities. Would really like to include some information in the capability (in the clear) to permit this match to be tested locally - but without unnecessarily leaking more information than is necessary.
How to safely support debugging of processes. This would seem to require access to the various node tables, the process table, process dictionaries etc. However such access would certainly provide a means of thwarting the information hiding provided by capabilities. Some way of safely enabling such access when appropriate, and disabling it otherwise, is needed.
Last, but certainly not least, is the question of whether to use encrypted capabilities or password (sparse) capabilities. Password (sparse) capabilities have the advantage of being easy to revoke, and have no cryptographic overhead. Their disadvantages are that they are larger (needing around 128 bits of randomness for security), and that they require each node to maintain a table mapping valid capabilities to their associated values and rights. Encrypted capabilities have the advantage of being smaller, and not requiring nodes to maintain a table; and disadvantages of being hard to revoke (unless separate shadow processes are used, which is costly), and that they require at least some cryptographic overhead (though this can perhaps be reduced by keeping clear version of capabilities in their home nodes).
A note, if encrypted capabilities are chosen, I strongly suggest considering the use of a keyed hash function for them. This would mean that the value and rights would be kept in the clear, but (along with the node name and type) be used to create a check value using the keyed hash function, which is then appended to the capability. Provided the underlying system is trusted never to use the values unless the check value is correct, this shouldn't compromise safety, though it does reveal a considerable amount of information.
In the short term there is a need to experiment with implementing some modest sized systems within the SSErl prototype to gain experience with, and try to validate, the mechanisms provided for creating custom security environments.
In the longer term, I would like to trial some of the variants suggested in the "Issues Raised" section, such as the use of the check function to automatically customise a number of interfaces for a custom node, or to perform a pre-execution proof against a supplied list of requirements (which I envision being a serious of calls of the check function with prototypical argument values).
The goal is to determine the most appropriate mechanisms to be implemented in a safer Erlang run-time system, from the range of options currently being considered.
In a recent (Mar 97) note on "Extensible Security Architectures for Java", Dan Wallach from the Princeton Uni "Secure Internet Programming" group has suggested some approaches include adding a capability mechanism (based around the Java objects), using extended stack introspection to examine class records on the stack at runtime to check their owning principles, and the use of type hiding. I find this interesting in the light of the approaches we've taken to designing a safer Erlang.
It also raises a number of issues under debate, and gives some suggestions for further work.