Lookup Users by Certificate

Related ticket(s):

Problem statement

As stated in ticket #2596 applications doing user authentication based on certificates, e.g. web servers, need a way to map the certificate presented by the client to a specific user. Although there are various ways to derive a user name from special entries in the certificate so far there is no generally accepted scheme. The most general and in some cases the only possible way is to look up the certificate directly in the LDAP server. This requires that the certificate is stored in the LDAP server which we will assume for this initial design. (In a second part user lookups based on the certificate content will be added, this requires that the syntax for the mapping is specified in http://www.freeipa.org/page/V4/User_Certificates#Certificate_Identity_Mapping)

The primary interface to lookup users by certificate would be D-BUS.

Use cases

The primary use case is described in ticket #2596. If Apache is configured to use certificate based client authentication modules like mod_lookup_identity has access to the PAM encoded certificate via environment variables. With this data as input mod_lookup_identity should call a D-BUS method like org.freedesktop.sssd.infopipe.GetUserAttrByCert which will return the data of the user the certificate belongs similar to the GetUserAttr method.

Overview of the solution

Besides adding the D-Bus method to the InfoPipe responder the generic LDAP backend should be able to search and read the certificate data if available from a LDAP server and store it to the cache. The internal sysdb interface must be extended to search cached entries with the certificate as input.

Implementation details

LDAP backend

Reading certificate data if available just requires adding a new user attribute which will be requested during LDAP searches for a user. In general the certificate is stored as a DER encoded binary on the LDAP server. Question: should we add an option like ldap_user_cert_encoding to support other encodings a server might send to us, or shall we add it only when there is a real use case? Internally the certificate should be stored DER encoded to the cache as well because this encoding is the most unambiguous encoding (e.g. with PEM encoding it is not clear if the base64 blob should have line breaks or not and if the enclosing ‘—–BEGIN CERTIFICATE—–’ and ‘—–END CERTIFICATE—–’ should be stored as well and if line break should be added here or not?)

To search a user with the help of the certificate the DER encoded binary ticket must be transformed into a search filter. In this case it would be something like ‘userCertificate=\23\a5\3e……’ where each byte from the certificate is represented by a hex value prepended by a ‘'. The filter should be generated in a subroutine which accepts the DER encoded certificate with base64 ASCII armor and returns the search filter. This way the subroutine can later be extended to accept configuration options for the identity mapping and can return different search filters for those cases. Since the requirement for LDAP and sysdb search filters are the same there should be an option indicating if a LDAP or sysdb filter is needed, because the attribute names might be different.

Although it would be possible to handle the binary DER data directly I think using a base64 ASCII armor to handle the data as a string is useful to avoid adding code for handling binaries e.g. in the S-BUS requests to the backends.

SYSDB API

A new call sysdb_search_user_by_cert() should be added which get the DER encoded certificate with base64 ASCII armor as input and use the function described above to get a proper search filter. Currently it will be only the search filter for the binary certificate. Other than that the new call will act like to other sysdb_search_user_by_*() calls.

InfoPipe

A new method GetUSerAttrByCert() must be implemented which expected the PEM encoded certificate and an array of attribute names. Question: Should we only support PEM here or other formats as well? In this case we need a third parameter indicating the encoding of the certificate data.. InfoPipe will convert the certificate into DER encoding with base64 ASCII armor, search the cache and eventually forward the request to the backend. The request to the backend is processed similar to a request by name, only that a new filter name, e.g. DP_CERT_ID “cert”, is needed.

Since it is in general not obvious to which domain a certificate belongs, the search must iterate over all domains in case no matching certificate was found. For the cases where there is a strong 1:1 relationship between the issuer of a certificate and a domain, configuration options for this can be added later.

Configuration changes

A new user attribute open ‘ldap_user_certificate’ will be added to the LDAP provider. By default only the IPA provider will set a value for it to avoid reading about 1k of data which is not needed in the other providers. Question: Does this make sense or shall we enable it for other providers as well?

How To Test

First a certificate must be load to a IPA user entry, it can be any kind of certificate as long as it is valid an DER or PEM encoded. Until IPA has some import utilities ldapmodify should be used. A LDIF file might look like this:

dn: uid=cert_user,cn=users,cn=accounts,dc=ipa,dc=devel
changetype: modify
add: userCertificate;binary
userCertificate;binary::MII...=

where MII…= indicates the base64 encoded certificate data. If you have a PEM encoded certificate you can just use the base64 part here. If the certificate is DER encoded it can be transformed to base64 with

base64 < ./certificate_file.der | tr -d '\n'

Testing can be done with the help of the dbus-send utility:

# dbus-send --system --print-reply  --dest=org.freedesktop.sssd.infopipe \
                                         /org/freedesktop/sssd/infopipe/Users \
                                         org.freedesktop.sssd.infopipe.Users.FindByCertificate \
                                         string:"-----BEGIN CERTIFICATE-----.......-----END CERTIFICATE-----"
method return sender=:1.1479 -> dest=:1.1498 reply_serial=2
   object path "/org/freedesktop/sssd/infopipe/Users/ipa_2edevel/240600004"

# dbus-send --system --print-reply --dest=org.freedesktop.sssd.infopipe /org/freedesktop/sssd/infopipe/Users/ipa_2edevel/240600004 org.freedesktop.DBus.Properties.Get string:"org.freedesktop.sssd.infopipe.Users.User" string:"name"
method return sender=:1.1479 -> dest=:1.1529 reply_serial=2
   variant       string "cert_user"

# dbus-send --system --print-reply --dest=org.freedesktop.sssd.infopipe /org/freedesktop/sssd/infopipe/Users/ipa_2edevel/240600004 org.freedesktop.DBus.Properties.GetAll string:"org.freedesktop.sssd.infopipe.Users.User"
method return sender=:1.1479 -> dest=:1.1530 reply_serial=2
   array [
      dict entry(
         string "name"
         variant             string "cert_user"
      )
      dict entry(
         string "uidNumber"
         variant             uint32 240600004
      )
      dict entry(
         string "gidNumber"
         variant             uint32 240600004
      )
      dict entry(
         string "gecos"
         variant             string "ipa u1"
      )
      dict entry(
         string "homeDirectory"
         variant             string "/home/cert_user"
      )
      dict entry(
         string "loginShell"
         variant             string "/bin/sh"
      )
      dict entry(
         string "groups"
         variant             array [
               object path "/org/freedesktop/sssd/infopipe/Groups/ipa_2edevel/240600004"
               object path "/org/freedesktop/sssd/infopipe/Groups/ipa_2edevel/240600005"
               object path "/org/freedesktop/sssd/infopipe/Groups/ipa_2edevel/240600006"
            ]
      )
      dict entry(
         string "extraAttributes"
         variant             array [
            ]
      )
   ]

The first dbus-send command shows the lookup by certificate, the following two just illustrate how a single property or all can be requested from the returned object path.

Authors