Rpc_intro_gss

Securing RPC with the GSSAPI

This text explains how to enable strong authentication and strong encryption on RPC connections.

The GSSAPI

The GSSAPI (Generic Security Service API) is an interface between the security provider (i.e. the authentication/encryption provider) and the application needing security features. The GSSAPI is a standard (RFC 2743). The GSSAPI is often already implemented by operating systems or by security systems like Kerberos. This means, there is usually a library on the C level containing the C functions defining GSSAPI.

The nice thing about GSSAPI is the mechanism genericy: The API can provide access to multiple security mechanisms, and it is possible to wrap almost every security mechanism in the form of GSSAPI.

As mentioned, GSSAPI only covers the encryption and integrity protection of messages, not of continuous streams. In so far there is not much intersection with SSL, which only handles such streams. For message-oriented protocols such as RPC the feature set of GSSAPI is naturally the better choice. There is a standard called RPCSEC-GSS defining the details of how GSSAPI is to be used for ONCRPC (RFC 2203).

There is a more general introduction to the GSSAPI in this chapter: Gssapi.

The GSSAPI in Ocaml

The GSSAPI is defined as a class type Netsys_gssapi.gss_api. We do not want to go much into detail - for using the GSSAPI it is not required to understand everything. The class type is "feature compatible" with the standard C version of the API (RFC 2744) allowing it to interface with implementations of GSSAPI available in C. (Note that this has not been done when this text is written.)

A class type has been chosen because this allows it that each security provider can define an independent class implementing the GSSAPI. This is different than in the C bindings of GSSAPI where only one provider can exist at a time (linked into the program), although the provider can manage several mechanisms.

When using GSSAPI you will be confronted with OIDs, names, and credentials. These concepts are defined in the Netsys_gssapi module:

OIDs are represented as oid = int array. There are a number of predefined OIDs, e.g.

An empty array is often used to mean the default (e.g. default mechanism).

Names are represented as Netsys_gssapi.name. This object has almost no methods - which is intended because names are opaque to users outside the GSSAPI implementation. The GSSAPI defines methods to import and export names:

Note that, at least for certain security mechanisms, there may be several ways of writing the name of a principal, or there might be naming elements spanning several mechanisms. This is an issue when names need to be compared. Generally, it may lead to wrong results when names are compared by displaying or exporting them, and then comparing the resulting strings. There is a special Netsys_gssapi.gss_api.compare_name method for comparisons, and Netsys_gssapi.gss_api.canonicalize_name may also be useful in this context.

For RPC, the ways of referring to names have been simplified - more on that below.

Credentials are also opaque objects - Netsys_gssapi.credential. It is generally assumed that a GSSAPI implementation can look up the right credentials for a principal that is identified by name. For example, the GSSAPI provider for SCRAM can be equipped with a "keyring", i.e. a callback that maps user names to passwords.

SCRAM

SCRAM (Salted Challenge Response Authentication Mechanism) is a relatively new security mechanism (RFC 5802) with interesting properties:

There is an extension for SCRAM so that AES-128 encryption and SHA-1 integrity protection become available in GSSAPI context.

SCRAM is implemented in Netmech_scram. The GSSAPI encapsulation is done in Netmech_scram_gssapi.

Some more words on names and credentials: Clients have to impersonate as a user, given by a simple unstructured string. The RFC requires that this string is UTF-8, and that certain Unicode normalizations need to be applied before use. This is not implemented right now (SASLprep is missing). Because of this, only US-ASCII user names are accepted. The same applies to the passwords.

In SCRAM, the client needs to know the password in cleartext. The server, however, usually only stores a triple

 (salted_password, salt, iteration_count) 

in the authentication database. The iteration_count is a constant defined by the server (should be >= 4096). The salt is a random string that is created when the user entry is added to the database. The function Netmech_scram.create_salt can be used for this. The salted_password can be computed from the two other parameters and the password with Netmech_scram.salt_password.

The GSSAPI encapsulation of SCRAM is Netmech_scram_gssapi.scram_gss_api. This class

class scram_gss_api : 
        ?client_key_ring:client_key_ring ->
        ?server_key_verifier:server_key_verifier ->
        Netmech_scram.profile ->
          Netsys_gssapi.gss_api

takes a few arguments. The profile can be just obtained by calling

Netmech_scram.profile `GSSAPI

which is usally the right thing here (one can also set a few parameters at this point). Depending on whether the class is needed for clients or servers, one passes either client_key_ring or server_key_verifier.

Netmech_scram_gssapi.client_key_ring is an object like

let client_key_ring =
  object
    method password_of_user_name user =
      match user with
       | "guest" -> "guest"
       | "gerd" -> "I won't reveal it"
       | _ -> raise Not_found

    method default_user_name = Some "guest"
  end

that mainly returns the passwords of users and that optionally defines a default user. (E.g. the default user could be set to the current login name of the process running the client.)

Netmech_scram_gssapi.server_key_verifier provides the credentials for password verification, e.g.

let server_key_verifier =
  object
    method scram_credentials user =
      match user with
       | "guest" ->
            ("\209\002U?,/Vu\253&\140\196j\158{b]\221\140\029", 
             "68bd268fe5e948a7e171a4df9ef6450a", 
             4096)
       | "gerd" ->
            ("\135\202\182P\142\r\175?\222\156\201bA\188\1296\154\197v\142",
             "5e51d100ace8d1a69cd4d015ac5da947", 
             4096)
       | _ -> raise Not_found
  end

Enabling SCRAM for RPC clients

Basically, an RPC client is created by a call like

let client = Rpc_client.create2 m prog esys

or by invoking ocamlrpcgen-created wrappers of this call. How can we enable SCRAM authentication?

Assumed we already created the gss_api object by instantiating the class Netmech_scram_gssapi.scram_gss_api this is done in two steps:

Optionally, one can also do a third step:

That's it!

Of course, am can be shared by several clients. This does not mean, however, that the clients share the security contexts. For each client a separate context is created (i.e. the authentication protocol starts from the beginning).

Both TCP and UDP are supported. Note that especially for UDP there might be issues with retransmitted client requests after running into timeouts. The problem is that retransmitted requests and the following responses look different on the wire than the original messages, and because of this the client can only accept a response when it is the response to the latest retransmission. This makes the retransmission feature less reliable. Best is to avoid UDP.

The function Rpc_auth_gssapi.client_auth_method takes a cconf object controlling details of the security context creation. This object is returned by Netsys_gssapi.create_client_config:

SCRAM is a simple security mechanism, and for advanced ones you may need to set more objects in cconf. In particular for Kerberos, it is required to set target_name to the server principal.

You might have wondered why we pass

~user_name_interpretation:(`Plain_name Netsys_gssapi.nt_user_name)

to client_auth_method. As described above, there are various ways how to represent names. In the RPC context we need a simple string. The user_name_interpretation argument selects how the opaque GSSAPI names are converted to strings.

Enabling SCRAM for RPC proxies

The Rpc_proxy module is a higher-level encapsulation of RPC clients providing additional reliability features. One can also configure the proxies to use authentication:

This could e.g. look like

let config =
  Rpc_proxy.ManagedClient.create_mclient_config
    ...
    ~auth_methods:[am]
    ~user_name:(Some "gerd")
    ...
    ()

The config value can then, as usual, be passed to Rpc_proxy.ManagedClient.create_mclient.

Enabling SCRAM for RPC servers

The general procedure for enabling authentication is similar to that in client context:

The method am can be shared by several servers.

Each connection to a server normally opens a new security context (or better, the context handles are kept private per connection). There is a special mode, however, permitting a more liberal setting: By passing ~shared_context:true to Rpc_auth_gssapi.server_auth_method independent connections can share security contexts if they know the security handles. Although the Ocamlnet client does not support this mode, it might be required for interoperability with other implementations. Also, for UDP servers this mode must be enabled - each UDP request/response pair is considered as a new connection by the RPC server (in some sense this is a peculiarity of the implementation).

You can get the name of the authenticated user with the function Rpc_server.get_user. The way of translating opaque GSSAPI names to strings can be selected with the ~user_name_format argument of Rpc_auth_gssapi.server_auth_method.

The sconf object has, like in the client case, a number of additional options. By setting ~privacy:`Required one can demand that only privacy-protected messages are accepted. ~integrity:`Required demands that at least integrity-protected messages are used. For more advanced mechanisms than SCRAM more options may be needed. In particular, for Kerberos you need at least to set the acceptor_name to the name of the Kerberos principal of the server.

Enabling SCRAM in Netplex context

The question is where to call Rpc_server.set_auth_methods.

RPC services are created by using Rpc_netplex.rpc_factory. This function has an argument setup which is a callback for configuring the server. Usually, this callback is used to bind the RPC procedure functions to the server object. This is also the ideal place to set the authentication method.

Pitfall: Note that setup may also be called for dummy servers that are not connected to real file descriptors. Netplex does this to find out how the server will be configured (especially it is interested in the list of procedures). If this is an issue you can test for a dummy server with Rpc_server.is_dummy.

Security considerations

SCRAM seems to be an excellent choice for a password-based authentication protocol. Of course, it has all the well-known weaknesses of the password approach (e.g. dictionary attacks are possible), but otherwise it is certainly state of the art.

The RPC messages are encrypted with AES-128. This is not configurable.

Integrity protection is obtained by using SHA-1 hashes. This is also not configurable.

Some parts of the RPC messages are not fully protected: Headers and error responses. This means that the numbers identifying the called RPC procedures are not privacy-protected. They are only integrity-protected.

Error responses are completely unprotected.