Plasma GitLab Archive
Projects Blog Knowledge

{1:intro Securing RPC with the GSS-API}

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

{2 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).


{2 The GSS-API in Ocaml}

The GSS-API is defined as a class type {!Netsys_gssapi.gss_api}. We do not
want to go much into detail - for {b 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 {!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, 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.

- {!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 GSS-API implementation. The GSS-API 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 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.


{2 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 ->
          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
]}


{2 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 Netsys_gssapi.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 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
GSS-API names are converted to strings.

{2 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}.


{2 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 {b 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.


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

{b 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}.


{2 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