Ocamlnet 3 Reference Manual : Rpc_intro_gss

# Rpc_intro_gss

# Securing RPC with the GSS-API

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

## The GSS-API

The GSS-API (Generic Security Service API) is an interface between the security provider (i.e. the authentication/encryption provider) and the application needing security features. The GSS-API is a standard (RFC 2743). The GSS-API 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 GSS-API.

The nice thing about GSS-API 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 GSS-API.

There is a "competitor" to GSS-API, namely SASL. The feature set is not identical, though. Both APIs allow strong authentication. GSS-API additionally includes encryption and integrity protection for message-oriented communication (but not for continuous data streams), whereas SASL does not have such a feature and has to rely on external layers to do so (which is often SSL). There is a bridge between GSS-API and SASL called GS2 - it translates the authentication part of GSS-API into SASL.

As mentioned, GSS-API 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 GSS-API is naturally the better choice. There is a standard called RPCSEC-GSS defining the details of how GSS-API is to be used for ONCRPC (RFC 2203).

## The GSS-API in Ocaml

The GSS-API is defined as a class type `Netgssapi.gss_api`. We do not want to go much into detail - for using the GSS-API 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 GSS-API 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 GSS-API. This is different than in the C bindings of GSS-API where only one provider can exist at a time (linked into the program), although the provider can manage several mechanisms.

When using GSS-API you will be confronted with OIDs, names, and credentials. These concepts are defined in the `Netgssapi` module:

- An OID (object identifier) is a IANA-registered number helping to identify especially security mechanisms, and styles of naming principals (user and system identities). OIDs are also used in other contexts (e.g. ASN-1, X500).
- Names come in various forms, and because of this, GSS-API uses opaque objects for names, not strings. Names can e.g. identify users. There are various styles (name types). For example, users are often identified by a simple string ("guest") whereas service names also include the host name where the service runs ("emailserver@machine"). As names are opaque, they can be imported from a string representation to the opaque object and they can be converted back to strings.
- Credentials are pieces of information allowing the security mechanism to check whether a connected participant has actually a certain name. A simple example is a password.

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

- `Netgssapi.nt_user_name` is the OID of the name type identifying users by name
- `Netgssapi.nt_hostbased_service` is the OID of the name type identifying services relative to hosts
- `Netmech_scram_gssapi.scram_mech` is the OID of the SCRAM security mechanism

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

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

- `Netgssapi.gss_api.import_name` allows one to convert a pair of a name type and a string to the opaque `Netgssapi.name` object. The name type is an OID.
- `Netgssapi.gss_api.display_name` is roughly the reverse operation (but see below).
- `Netgssapi.gss_api.export_name` converts an opaque name to a binary string. It is ensured that another call of `import_name` with the same string restores the opaque name.

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 `Netgssapi.gss_api.compare_name` method for comparisons, and `Netgssapi.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 - `Netgssapi.credential`. It is generally assumed that a GSS-API implementation can look up the right credentials for a principal that is identified by name. For example, the GSS-API 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:

- It is a password-based authentication scheme
- No complicated helpers like certification authorities or ticket servers are required for deployment
- The password is not transmitted during authentication (because of the challenge/response style)
- The server needs not to store the password in cleartext. Only the salted password is needed, and it is not possible to use a salted password on the client (i.e. it is fruitless to steal the password database)
- Not only the client authenticates to the server, but also vice versa - the protocol proves to the client that the server has access to the salted password
- The server does not have a name

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

SCRAM is implemented in `Netmech_scram`. The GSS-API 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 GSS-API 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 ->
              Netgssapi.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:

- Create the authentication method on RPC level:

        let am =
          Rpc_auth_gssapi.client_auth_method
            ~user_name_interpretation:(`Plain_name Netgssapi.nt_user_name)
            gss_api Netmech_scram.scram_mech

- Add this method to the client:

        Rpc_client.set_auth_methods client [am]

Optionally, one can also do a third step:

- Set the user (if you do not want to impersonate the default user):

        Rpc_client.set_user_name client (Some "gerd")

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` has a few optional arguments controlling whether encryption or integrity protection are enabled:

- By setting `~privacy:`Required` it is ensured that encryption and integrity protection are both enabled. If the security mechanism does not provide this, the function fails.
- The default is `~privacy:`If_possible`. This means that privacy is enabled if the mechanism supports it.
- There is a second argument `~integrity` only controlling integrity protection. It comes into play if full privacy is not available or if it is disabled with `~privacy:`None`. If integrity protection is on but full privacy is off the messages are not encrypted but only signed with a checksum.
- One can also turn both features off: `~privacy:`None` and `~integrity:`None`. In this case, the messages are neither enrypted nor protected. The authentication protocol at the beginning of a session is unaffected and is done nevertheless. This may be an option if you only want to authenticate a TCP connection but not protect the connection. For UDP it is strongly discouraged to use this mode - it is very easy to hack this.
- Of course, the RPC server has the last word which protection level is acceptable.

You might have wondered why we pass

    ~user_name_interpretation:(`Plain_name Netgssapi.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 GSS-API 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:

- Pass the authentication method `am` (as created above) in the mclient config with the argument `~auth_methods`, see `Rpc_proxy.ManagedClient.create_mclient_config`.
- Set the user in the mclient config with the argument `~user_name`

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:

- Create the authentication method on RPC level:

        let am =
          Rpc_auth_gssapi.server_auth_method
            ~user_name_format:`Plain_name
            gss_api Netmech_scram.scram_mech

- Add this method to the server:

        Rpc_server.set_auth_methods server [am]

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 GSS-API names to strings can be selected with the `~user_name_format` argument of `Rpc_auth_gssapi.server_auth_method`.

By setting `~require_privacy` one can demand that only privacy-protected messages are accepted. `~require_integrity` demands that at least integrity-protected messages are used.

## 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.