Ocamlnet 4 Reference Manual : Rpc_intro_gss

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

- 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, GSSAPI 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.

- `Netsys_gssapi.nt_user_name` is the OID of the name type identifying users by name
- `Netsys_gssapi.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 `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:

- `Netsys_gssapi.gss_api.import_name` allows one to convert a pair of a name type and a string to the opaque `Netsys_gssapi.name` object. The name type is an OID.
- `Netsys_gssapi.gss_api.display_name` is roughly the reverse operation (but see below).
- `Netsys_gssapi.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 `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:

- 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 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:

- Create the authentication method on RPC level:

        let cconf = Netsys_gssapi.create_client_config
                      ~mech_type:Netmech_scram.scram_mech () in
        let am =
          Rpc_auth_gssapi.client_auth_method
            ~user_name_interpretation:(`Plain_name Netsys_gssapi.nt_user_name)
            gss_api cconf

- 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` takes a `cconf` object controlling details of the security context creation. This object is returned by `Netsys_gssapi.create_client_config`:

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

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:

- 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 sconf = Netsys_gssapi.create_server_config
                      ~each_types:[Netmech_scram.scram_mech] () in
        let am =
          Rpc_auth_gssapi.server_auth_method
            ~user_name_format:`Plain_name
            gss_api sconf

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