This text explains how to enable strong authentication and strong encryption on RPC connections.
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 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:
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 nameNetgssapi.nt_hostbased_service
is the OID of the name type
identifying services relative to hostsNetmech_scram_gssapi.scram_mech
is the OID of the SCRAM
security 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.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 (Salted Challenge Response Authentication Mechanism) is a relatively new security mechanism (RFC 5802) with interesting properties:
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
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:
let am =
Rpc_auth_gssapi.client_auth_method
~user_name_interpretation:(`Plain_name Netgssapi.nt_user_name)
gss_api Netmech_scram.scram_mech
Rpc_client.set_auth_methods client [am]
Rpc_client.set_user_name client (Some "gerd")
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:
~privacy:`Required
it is ensured that encryption and
integrity protection are both enabled. If the security mechanism
does not provide this, the function fails.~privacy:`If_possible
. This means that
privacy is enabled if the mechanism supports it.~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.~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.
~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.
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:
am
(as created above) in the
mclient config with the argument ~auth_methods
, see
Rpc_proxy.ManagedClient.create_mclient_config
.~user_name
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
.
The general procedure for enabling authentication is similar to that in client context:
let am =
Rpc_auth_gssapi.server_auth_method
~user_name_format:`Plain_name
gss_api Netmech_scram.scram_mech
Rpc_server.set_auth_methods server [am]
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.
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
.
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.