Plasma GitLab Archive
Projects Blog Knowledge

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.

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:

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.

This web site is published by Informatikbüro Gerd Stolpmann
Powered by Caml