On Mon, Sep 19, 2022 at 09:54:56PM +1000, Jonathan Matthew wrote:
> This adds client certificate authentication to ypldap(8).  libtls makes the
> actual certificate part of this straightforward (I would still like it
> reviewed, though), but there are some LDAP complications.
>
> Depending on your LDAP server and how you connect to it (LDAPS on port 636
> or LDAP+TLS on port 389), a client presenting a certificate might
> automatically be bound as the subject of the certificate, or it might not.
> If it's not, the client can do an LDAP bind operation using the SASL
> EXTERNAL mechanism to bind as the cert subject, and it can optionally specify
> an identity, which means the bind will fail if the cert subject doesn't match
> that identity.  If the client didn't present a certificate, the bind will
> also fail (one would hope).
> 
> For reference, with Active Directory, SASL EXTERNAL bind is required when
> using LDAP+TLS, but not when using LDAPS, and the client identity can be
> specified in the form of "dn:" followed by the expected cert subject DN.
> OpenLDAP doesn't seem to do automatic bind at all, so SASL EXTERNAL would
> always be required there, and it doesn't appear to support specifying the
> expected identity with the bind.
> 
> The diff adds 'certfile' and 'keyfile' config directives for specifying
> the certificate to use, and a 'bindext' directive for enabling SASL EXTERNAL
> bind, optionally including the identity string.  SASL EXTERNAL bind doesn't
> get enabled implicitly when you configure a client cert, because ypldap
> can't tell if it's required or supported by the server.  It's also not an
> error to enable SASL EXTERNAL bind without a client cert, since you could be
> connecting through stunnel or something.
> 
> To configure this in ypldap.conf, you'd do something like this:
> 
> directory "ldap.example.com" tls {
>       bindext "dn:CN=ypldap,OU,Accounts,DC=example,DC=com"
>       certfile "/etc/ssl/ypldap-cert.pem"
>       keyfile "/etc/ssl/private/ypldap-key.pem"
> 
>       ...
> }
> 
> ok?

The libtls parts of this are of course fine. I do not have a way to test
this, but your explanations and the code make sense.

ok tb

Two minor comments below.

> 
> Index: aldap.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/ypldap/aldap.c,v
> retrieving revision 1.48
> diff -u -p -r1.48 aldap.c
> --- aldap.c   31 Mar 2022 09:06:55 -0000      1.48
> +++ aldap.c   19 Sep 2022 11:47:13 -0000
> @@ -220,6 +220,40 @@ fail:
>  }
>  
>  int
> +aldap_bind_sasl_external(struct aldap *ldap, char *bindid)
> +{
> +     struct ber_element *root = NULL, *elm;
> +
> +     if ((root = ober_add_sequence(NULL)) == NULL)
> +             goto fail;
> +
> +     elm = ober_printf_elements(root, "d{tds{ts", ++ldap->msgid,
> +         BER_CLASS_APP, LDAP_REQ_BIND, VERSION, "",
> +         BER_CLASS_CONTEXT, LDAP_AUTH_SASL, LDAP_SASL_MECH_EXTERNAL);

I think a NULL check for elm would be appropriate: while it's unlikely,
the subsequent ober_add_*() calls might hide an error in
ober_printf_elements().

> +     if (bindid == NULL)
> +             elm = ober_add_null(elm);
> +     else
> +             elm = ober_add_string(elm, bindid);
> +
> +     if (elm == NULL)
> +             goto fail;
> +
> +     LDAP_DEBUG("aldap_bind_sasl_external", root);
> +
> +     if (aldap_send(ldap, root) == -1) {
> +             root = NULL;
> +             goto fail;
> +     }
> +     return (ldap->msgid);
> +fail:
> +     if (root != NULL)

Since ber.c r1.32, this NULL check is no longer necessary. Perhaps all
such checks could be garbage collected in another diff.

> +             ober_free_elements(root);
> +
> +     ldap->err = ALDAP_ERR_OPERATION_FAILED;
> +     return (-1);
> +}
> +
> +int
>  aldap_unbind(struct aldap *ldap)
>  {
>       struct ber_element *root = NULL, *elm;
> Index: aldap.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/ypldap/aldap.h,v
> retrieving revision 1.14
> diff -u -p -r1.14 aldap.h
> --- aldap.h   11 May 2019 17:46:02 -0000      1.14
> +++ aldap.h   19 Sep 2022 11:47:13 -0000
> @@ -32,6 +32,8 @@
>  #define LDAP_PAGED_OID               "1.2.840.113556.1.4.319"
>  #define LDAP_STARTTLS_OID    "1.3.6.1.4.1.1466.20037"
>  
> +#define LDAP_SASL_MECH_EXTERNAL      "EXTERNAL"
> +
>  struct aldap {
>  #define ALDAP_ERR_SUCCESS            0
>  #define ALDAP_ERR_PARSER_ERROR               1
> @@ -137,6 +139,7 @@ enum deref_aliases {
>  
>  enum authentication_choice {
>       LDAP_AUTH_SIMPLE        = 0,
> +     LDAP_AUTH_SASL          = 3,
>  };
>  
>  enum scope {
> @@ -222,6 +225,7 @@ void                       aldap_freemsg(struct 
> aldap_messa
>  int                   aldap_req_starttls(struct aldap *);
>  
>  int   aldap_bind(struct aldap *, char *, char *);
> +int   aldap_bind_sasl_external(struct aldap *, char *);
>  int   aldap_unbind(struct aldap *);
>  int   aldap_search(struct aldap *, char *, enum scope, char *, char **, int, 
> int, int, struct aldap_page_control *);
>  int   aldap_get_errno(struct aldap *, const char **);
> Index: ldapclient.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/ypldap/ldapclient.c,v
> retrieving revision 1.45
> diff -u -p -r1.45 ldapclient.c
> --- ldapclient.c      22 Aug 2022 10:10:59 -0000      1.45
> +++ ldapclient.c      19 Sep 2022 11:47:13 -0000
> @@ -635,7 +635,11 @@ client_try_idm(struct env *env, struct i
>               int rc;
>  
>               where = "binding";
> -             if (aldap_bind(al, idm->idm_binddn, idm->idm_bindcred) == -1)
> +             if (idm->idm_bindext != 0)
> +                     rc = aldap_bind_sasl_external(al, idm->idm_bindextid);
> +             else
> +                     rc = aldap_bind(al, idm->idm_binddn, idm->idm_bindcred);
> +             if (rc == -1)
>                       goto bad;
>  
>               where = "parsing";
> Index: parse.y
> ===================================================================
> RCS file: /cvs/src/usr.sbin/ypldap/parse.y,v
> retrieving revision 1.35
> diff -u -p -r1.35 parse.y
> --- parse.y   19 Aug 2022 03:50:32 -0000      1.35
> +++ parse.y   19 Sep 2022 11:47:13 -0000
> @@ -108,7 +108,7 @@ typedef struct {
>  %token       USER GROUP TO EXPIRE HOME SHELL GECOS UID GID INTERVAL
>  %token       PASSWD NAME FIXED LIST GROUPNAME GROUPPASSWD GROUPGID MAP
>  %token       INCLUDE DIRECTORY CLASS PORT ERROR GROUPMEMBERS LDAPS TLS CAFILE
> -%token       BIND LOCAL PORTMAP 
> +%token       BIND LOCAL PORTMAP BINDEXT CERTFILE KEYFILE
>  %token       <v.string>      STRING
>  %token  <v.number>   NUMBER
>  %type        <v.number>      opcode attribute
> @@ -213,6 +213,11 @@ attribute        : NAME                                  
> { $$ = 0; }
>               ;
>  
>  diropt               : BINDDN STRING                         {
> +                     if (idm->idm_bindext != 0) {
> +                             yyerror("can't specify multiple bind types");
> +                             free($2);
> +                             YYERROR;
> +                     }
>                       idm->idm_flags |= F_NEEDAUTH;
>                       if (strlcpy(idm->idm_binddn, $2,
>                           sizeof(idm->idm_binddn)) >=
> @@ -224,6 +229,11 @@ diropt           : BINDDN STRING                         
> {
>                       free($2);
>               }
>               | BINDCRED STRING                       {
> +                     if (idm->idm_bindext != 0) {
> +                             yyerror("can't specify multiple bind types");
> +                             free($2);
> +                             YYERROR;
> +                     }
>                       idm->idm_flags |= F_NEEDAUTH;
>                       if (strlcpy(idm->idm_bindcred, $2,
>                           sizeof(idm->idm_bindcred)) >=
> @@ -234,6 +244,64 @@ diropt           : BINDDN STRING                         
> {
>                       }
>                       free($2);
>               }
> +             | BINDEXT STRING                        {
> +                     if (idm->idm_flags & F_NEEDAUTH) {
> +                             yyerror("can't specify multiple bind types");
> +                             free($2);
> +                             YYERROR;
> +                     }
> +                     idm->idm_flags |= F_NEEDAUTH;
> +                     idm->idm_bindext = 1;
> +                     if (strlcpy(idm->idm_bindextid, $2,
> +                         sizeof(idm->idm_bindextid)) >=
> +                         sizeof(idm->idm_bindextid)) {
> +                             yyerror("directory bindext truncated");
> +                             free($2);
> +                             YYERROR;
> +                     }
> +                     free($2);
> +             }
> +             | BINDEXT                               {
> +                     if (idm->idm_flags & F_NEEDAUTH) {
> +                             yyerror("can't specify multiple bind types");
> +                             YYERROR;
> +                     }
> +                     idm->idm_flags |= F_NEEDAUTH;
> +                     idm->idm_bindext = 1;
> +                     idm->idm_bindextid[0] = '\0';
> +             }
> +             | CERTFILE STRING                       {
> +                     if (idm->idm_tls_config == NULL) {
> +                             yyerror("can't set cert file without tls"
> +                                 " enabled");
> +                             free($2);
> +                             YYERROR;
> +                     }
> +                     if (tls_config_set_cert_file(idm->idm_tls_config, $2)
> +                         == -1) {
> +                             yyerror("tls set cert file failed: %s",
> +                                 tls_config_error(
> +                                 idm->idm_tls_config));
> +                             free($2);
> +                             YYERROR;
> +                     }
> +             }
> +             | KEYFILE STRING                        {
> +                     if (idm->idm_tls_config == NULL) {
> +                             yyerror("can't set key file without tls"
> +                                 " enabled");
> +                             free($2);
> +                             YYERROR;
> +                     }
> +                     if (tls_config_set_key_file(idm->idm_tls_config, $2)
> +                         == -1) {
> +                             yyerror("tls set key file failed: %s",
> +                                 tls_config_error(
> +                                 idm->idm_tls_config));
> +                             free($2);
> +                             YYERROR;
> +                     }
> +             }
>               | BASEDN STRING                 {
>                       if (strlcpy(idm->idm_basedn, $2,
>                           sizeof(idm->idm_basedn)) >=
> @@ -460,7 +528,9 @@ lookup(char *s)
>               { "bind",               BIND },
>               { "bindcred",           BINDCRED },
>               { "binddn",             BINDDN },
> +             { "bindext",            BINDEXT },
>               { "cafile",             CAFILE },
> +             { "certfile",           CERTFILE },
>               { "change",             CHANGE },
>               { "class",              CLASS },
>               { "directory",          DIRECTORY },
> @@ -479,6 +549,7 @@ lookup(char *s)
>               { "home",               HOME },
>               { "include",            INCLUDE },
>               { "interval",           INTERVAL },
> +             { "keyfile",            KEYFILE },
>               { "ldaps",              LDAPS },
>               { "list",               LIST },
>               { "local",              LOCAL },
> Index: ypldap.conf.5
> ===================================================================
> RCS file: /cvs/src/usr.sbin/ypldap/ypldap.conf.5,v
> retrieving revision 1.27
> diff -u -p -r1.27 ypldap.conf.5
> --- ypldap.conf.5     22 Aug 2022 07:07:45 -0000      1.27
> +++ ypldap.conf.5     19 Sep 2022 11:47:13 -0000
> @@ -144,6 +144,9 @@ or
>  attribute to the LDAP attribute name supplied.
>  .It Ic basedn Ar string
>  Use the supplied search base as starting point for the directory search.
> +.It Ic certfile Ar string
> +Use the specified client certificate when connecting to the directory.
> +The file must contain a PEM encoded certificate.
>  .It Ic groupdn Ar string
>  Use the supplied search base as starting point for the directory search for
>  groups.
> @@ -152,12 +155,23 @@ If not supplied, the basedn value will b
>  Use the supplied credentials for simple authentication against the directory.
>  .It Ic binddn Ar string
>  Use the supplied Distinguished Name to bind to the directory.
> +.It Ic bindext Oo Ar string Oc
> +Bind to the directory using SASL EXTERNAL, optionally using a supplied 
> identity
> +string.
> +When using a TLS client certificate, this allows the client to bind as the
> +subject of the certificate.
> +If an identity string is supplied, usually in the form of a distinguished 
> name
> +prefixed with "dn:", the directory will only allow the bind to succeed if it
> +matches the subject of the certificate.
>  .It Ic fixed attribute Ar attribute string
>  Do not retrieve the specified attribute from LDAP but
>  instead set it unconditionally to the supplied value for
>  every entry.
>  .It Ic group filter Ar string
>  Use the supplied LDAP filter to retrieve group entries.
> +.It Ic keyfile Ar string
> +Use the specified private key when connecting to the directory.
> +The file must contain a PEM encoded key.
>  .It Xo
>  .Ic list Ar name Ic maps to Ar string
>  .Xc
> Index: ypldap.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/ypldap/ypldap.h,v
> retrieving revision 1.22
> diff -u -p -r1.22 ypldap.h
> --- ypldap.h  19 Aug 2022 03:50:32 -0000      1.22
> +++ ypldap.h  19 Sep 2022 11:47:13 -0000
> @@ -101,7 +101,9 @@ struct idm {
>       u_int32_t                        idm_list;
>       struct ypldap_addr_list          idm_addr;
>       in_port_t                        idm_port;
> +     int                              idm_bindext;
>       char                             idm_binddn[LINE_WIDTH];
> +     char                             idm_bindextid[LINE_WIDTH];
>       char                             idm_bindcred[LINE_WIDTH];
>       char                             idm_basedn[LINE_WIDTH];
>       char                             idm_groupdn[LINE_WIDTH];
> 

Reply via email to