Custom Security Policies in SSErl

Now Superceeded by Custom Safety Policies in SSErl


Dr Lawrie Brown
School of Computer Science, Australian Defence Force Academy, Canberra, Australia
Email: Lawrie.Brown@adfa.edu.au

Last updated: 24 April 1997.


Abstract

This report describes an initial approach to implementing a specified security policy in an SSErl node. 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.

Introduction

SERCs Safer Erlang (SSErl) is a prototypical implementation of what I believe to be a simpler and more comprehensive design of a secure erlang execution environment. It is currently implemented as a collection of glue functions substituted by a modified erlang compiler for all calls of "unsafe" BIFs. These interact with "node" server processes, one for each distinct (sub)node on the erlang system. The prototype supports capabilities for pids, ports, and nodes; and a hierarchy of subnodes on each erlang system, see [Bro97a].

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.

Implementing a Security Monitor

A security monitor facility has been added to SSErl. This consists of a modification of the apply BIF glue routine, so that an additional function may (optionally) be called before any external function call. This permits all such calls to be vetted against a security policy. This monitor function can either return ok, or raise an exception to reject the call. The Erlang compiler (specifically sys_pre_expand) has been modified to add the calling module name as an extra argument to the 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.

Policy Module

For ease of use, policy for an SSErl node may be specified by a policy module of a prescribed form. This module must contain and export a number of functions which are used to create the SSErl node with the desired security policy. These functions are:
max_nrights()
returns the maximal list of rights for node capabilities. The actual rights may be further restricted by the parent node.
max_prights()
returns the maximal list of rights for process capabilities. The actual rights may be further restricted by the parent node.
aliases()
returns the list of aliases to be used in the new node. Usually these will map to modified versions of standard library modules.
init_servers()
calls any server init routines servicing the new node, these servers are run in the current node context. These servers are usually found by the appropriate inheritance of a registered name into the new node.
names()
returns the list of registered names to inherit, usually belonging to servers started by init_servers.
check(From,M,F,A)
the apply check security monitor function, called on all applys. It should return ok | {EXIT,{policy_violation,{apply,M,F,A}}. NOTE - this function MUST NOT call any external routines, to prevent circular dependencies.

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}}).

Creating a Node Implementing a Policy

To create a new node which implements the policy in a policy module, a utility function 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.

Issues Raised

A number of issues came to light whilst experimenting with this approach.

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.

Some SSErl Features in more Detail

The following describes in some more detail, the interfaces to some of the new BIFs added to the SSErl prototype, particularly those related to subnodes and capabilities.
newnode(Parent,Name,{Node_Rights,Proto_Rights,Names,Aliases,Options,Apply_Chk})
creates a new SSErl node with the specified parameters. Any values not supplied (given as nil) will be inherited from the parent node.
newnode(Parent,Name)
newnode(Name)
variants which inherit all details from the specified parent/current node.
node_info(NodeCapa)
returns the node information table - probably releases too much information, but is needed for debugging!?!?!
shutdown(CNode)
shuts a node, and all subnodes and processes within it, down.
check(Capa,Op)
checks if the supplied capability permits the requested operation, returns true or generates an exception.
make_ref()
make_ref(Type,Val)
creates a reference capability. The latter call associates a user Type (an atom) and Val (an atom or integer) with the reference capability for use as a user capability.
restrict(Rights)
restricts the list of rights for a processes self capability - does not create a new capability. nb. the new list of rights will be the intersection of the existing and supplied lists of rights.
restrict(Capa,Rights)
creates a new version of the supplied capability with a more restricted set of rights. nb. the new list of rights will be the intersection of the existing and supplied lists of rights.
revoke(Capa,Master)
revokes the capability Capa, which must be a restricted capability, and must refer to the same object as the Master capability.
revoke(Capa)
revokes a capability restricted from the processes self capability.
same(Capa1,Capa2)
tests whether the two supplied capabilities refer to the same object nb. currently this requires a query to each of the capabilities nodes, hence it cannot be used in a guard.
view(Capa)
returns the value associated with the specified capability (ie {Class,Pid,Rights}).

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:

ping NodeName
returns the main node capability for the named node if it exists. Currently used for easy access to remote nodes, but definitely a safety hole.

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:

policynode(Name,Policy)
creates a subnode with the supplied Name, which implements the named Policy module
read_capa(FileName)
reads a capability from the named file (where it must be written as a binary term, assuming file access is permitted)
write_capa(FileName,Capa)
writes the given capability to the named file (as a binary term)
The latter two may be used to transfer a capability between distinct Erlang systems (nodes). nb. cookies must also be exchanged in the current prototype.

Topics of Heated Debate

I believe there is a general agreement on the need to add a concept of capabilities to protect access to processes, nodes and external resources; and a concept of subnodes to provide a distinct execution context; to Erlang. The details are, however, still subject to active debate. Some of the issues include:

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:

There is currently a vigourous debate with some colleagues at CSlab on whether both are appropriate, in particular whether the latter should be handled separately. This could be via a separate list of rights associated with a process or a node (as distinct from a capability). This would mean that the list of rights associated with capabilities would be much smaller. It could even be handled by using an extended version of the apply security monitor function (though great care would be needed to prevent dependencies in this function). This would have the advantage of a single uniform approach to specifying permitted operations though.

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.

Further Work

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.

Interesting Tidbits

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.

Conclusions

This paper describes an approach to implementing a desired security policy in an SSErl node via the use of a policy module which includes a security monitor function to check all apply calls.

It also raises a number of issues under debate, and gives some suggestions for further work.

Acknowledgements

The SSErl prototype and this paper were written during my special studies program in 1997, whilst visiting SERC in Melbourne, and NTNU in Trondheim, Norway. I'd like to thank my colleagues at these institutions, and at the Ericsson Computer Science Laboratory in Stockholm for their discussions and support.

References

Bro97a
L. Brown, "Introducing SERCs Safer Erlang", Australian Defence Force Academy, Canberra, Australia, Technical Note, Mar 1997. http://lpb.canb.auug.org.au/adfa/papers/ssp97/sserl97a.html.

Now Superceeded by Custom Safety Policies in SSErl